Bug 1197420 Part 3 Initial browser.permissions api support r=kmag
☠☠ backed out by 0fa8a7ec4b42 ☠ ☠
authorAndrew Swan <aswan@mozilla.com>
Fri, 24 Mar 2017 13:55:09 -0700
changeset 397712 46e135035f1049d81fabfc8c5c74c1b2d46f26b3
parent 397711 440bab1415095b4574672125d1786ad13766fad2
child 397713 925e3a9499ee1f8a4f17bea37179a1416846aa37
push id7391
push usermtabara@mozilla.com
push dateMon, 12 Jun 2017 13:08:53 +0000
treeherdermozilla-beta@2191d7f87e2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskmag
bugs1197420
milestone55.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1197420 Part 3 Initial browser.permissions api support r=kmag With this patch, permissions are not actually applied, but the permissions api is in place. MozReview-Commit-ID: CTaXz5sa1xy
browser/locales/en-US/chrome/browser/browser.properties
browser/modules/ExtensionsUI.jsm
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionPermissions.jsm
toolkit/components/extensions/ExtensionUtils.jsm
toolkit/components/extensions/ext-c-permissions.js
toolkit/components/extensions/ext-permissions.js
toolkit/components/extensions/extensions-toolkit.manifest
toolkit/components/extensions/jar.mn
toolkit/components/extensions/moz.build
toolkit/components/extensions/schemas/jar.mn
toolkit/components/extensions/schemas/permissions.json
toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
toolkit/modules/addons/MatchPattern.jsm
--- a/browser/locales/en-US/chrome/browser/browser.properties
+++ b/browser/locales/en-US/chrome/browser/browser.properties
@@ -79,16 +79,27 @@ webextPerms.updateMenuItem=%S requires n
 # LOCALIZATION NOTE (webextPerms.updateText)
 # %S is replaced with the localized name of the updated extension.
 # Note, this string will be used as raw markup. Avoid characters like <, >, &
 webextPerms.updateText=%S has been updated. You must approve new permissions before the updated version will install. Choosing “Cancel” will maintain your current add-on version.
 
 webextPerms.updateAccept.label=Update
 webextPerms.updateAccept.accessKey=U
 
+# LOCALIZATION NOTE (webextPerms.optionalPermsHheader)
+# %S is replace with the localized name of the extension requested new
+# permissions.
+# Note, this string will be used as raw markup. Avoid characters like <, >, &
+webextPerms.optionalPermsHeader=%S requests additional permissions.
+webextPerms.optionalPermsListIntro=It wants to:
+webextPerms.optionalPermsAllow.label=Allow
+webextPerms.optionalPermsAllow.accessKey=A
+webextPerms.optionalPermsDeny.label=Deny
+webextPerms.optionalPermsDeny.accessKey=D
+
 webextPerms.description.bookmarks=Read and modify bookmarks
 webextPerms.description.clipboardRead=Get data from the clipboard
 webextPerms.description.clipboardWrite=Input data to the clipboard
 webextPerms.description.downloads=Download files and read and modify the browser’s download history
 webextPerms.description.geolocation=Access your location
 webextPerms.description.history=Access browsing history
 # LOCALIZATION NOTE (webextPerms.description.nativeMessaging)
 # %S will be replaced with the name of the application
--- a/browser/modules/ExtensionsUI.jsm
+++ b/browser/modules/ExtensionsUI.jsm
@@ -36,16 +36,17 @@ this.ExtensionsUI = {
   histogram: null,
 
   init() {
     this.histogram = Services.telemetry.getHistogramById("EXTENSION_INSTALL_PROMPT_RESULT");
 
     Services.obs.addObserver(this, "webextension-permission-prompt", false);
     Services.obs.addObserver(this, "webextension-update-permissions", false);
     Services.obs.addObserver(this, "webextension-install-notify", false);
+    Services.obs.addObserver(this, "webextension-optional-permission-prompt", false);
 
     this._checkForSideloaded();
   },
 
   _checkForSideloaded() {
     AddonManager.getAllAddons(addons => {
       // Check for any side-loaded addons that the user is allowed
       // to enable.
@@ -198,16 +199,24 @@ this.ExtensionsUI = {
       this.emit("change");
     } else if (topic == "webextension-install-notify") {
       let {target, addon, callback} = subject.wrappedJSObject;
       this.showInstallNotification(target, addon).then(() => {
         if (callback) {
           callback();
         }
       });
+    } else if (topic == "webextension-optional-permission-prompt") {
+      let {browser, name, icon, permissions, resolve} = subject.wrappedJSObject;
+      let strings = this._buildStrings({
+        type: "optional",
+        addon: {name},
+        permissions,
+      });
+      resolve(this.showPermissionsPrompt(browser, strings, icon));
     }
   },
 
   // Escape &, <, and > characters in a string so that it may be
   // injected as part of raw markup.
   _sanitizeName(name) {
     return name.replace(/&/g, "&amp;")
                .replace(/</g, "&lt;")
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -76,16 +76,17 @@ XPCOMUtils.defineLazyServiceGetter(this,
 
 var {
   GlobalManager,
   ParentAPIManager,
   apiManager: Management,
 } = ExtensionParent;
 
 const {
+  classifyPermission,
   EventEmitter,
   LocaleData,
   StartupCache,
   getUniqueId,
   validateThemeManifest,
 } = ExtensionUtils;
 
 XPCOMUtils.defineLazyGetter(this, "console", ExtensionUtils.getConsole);
@@ -477,22 +478,21 @@ this.ExtensionData = class {
 
     let whitelist = [];
     for (let perm of this.manifest.permissions) {
       if (perm == "contextualIdentities" && !Preferences.get("privacy.userContext.enabled")) {
         continue;
       }
 
       this.permissions.add(perm);
-
-      let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
-      if (!match) {
+      let type = classifyPermission(perm);
+      if (type.origin) {
         whitelist.push(perm);
-      } else if (match[1] == "experiments" && match[2]) {
-        this.apiNames.add(match[2]);
+      } else if (type.api) {
+        this.apiNames.add(type.api);
       }
     }
     this.whiteListedHosts = new MatchPattern(whitelist);
 
     for (let api of this.apiNames) {
       this.dependencies.add(`${api}@experiments.addons.mozilla.org`);
     }
 
@@ -675,16 +675,17 @@ this.Extension = class extends Extension
 
     this.hasShutdown = false;
     this.onShutdown = new Set();
 
     this.uninstallURL = null;
 
     this.apis = [];
     this.whiteListedHosts = null;
+    this._optionalOrigins = null;
     this.webAccessibleResources = null;
 
     this.emitter = new EventEmitter();
   }
 
   static set browserUpdated(updated) {
     _browserUpdated = updated;
   }
@@ -1013,9 +1014,17 @@ this.Extension = class extends Extension
     }
 
     return this.permissions.has(perm);
   }
 
   get name() {
     return this.manifest.name;
   }
+
+  get optionalOrigins() {
+    if (this._optionalOrigins == null) {
+      let origins = this.manifest.optional_permissions.filter(perm => classifyPermission(perm).origin);
+      this._optionalOrigins = new MatchPattern(origins);
+    }
+    return this._optionalOrigins;
+  }
 };
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ExtensionPermissions.jsm
@@ -0,0 +1,111 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "JSONFile",
+                                  "resource://gre/modules/JSONFile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+                                  "resource://gre/modules/osfile.jsm");
+
+this.EXPORTED_SYMBOLS = ["ExtensionPermissions"];
+
+const FILE_NAME = "extension-preferences.json";
+
+let prefs;
+let _initPromise;
+function lazyInit() {
+  if (!_initPromise) {
+    prefs = new JSONFile({path: OS.Path.join(OS.Constants.Path.profileDir, FILE_NAME)});
+
+    _initPromise = prefs.load();
+  }
+  return _initPromise;
+}
+
+function emptyPermissions() {
+  return {permissions: [], origins: []};
+}
+
+this.ExtensionPermissions = {
+  async get(extension) {
+    await lazyInit();
+
+    let perms = emptyPermissions();
+    if (prefs.data[extension.id]) {
+      Object.assign(perms, prefs.data[extension.id]);
+    }
+    return perms;
+  },
+
+  // Add new permissions for the given extension.  `permissions` is
+  // in the format that is passed to browser.permissions.request().
+  async add(extension, perms) {
+    await lazyInit();
+
+    if (!prefs.data[extension.id]) {
+      prefs.data[extension.id] = emptyPermissions();
+    }
+    let {permissions, origins} = prefs.data[extension.id];
+
+    let added = emptyPermissions();
+
+    for (let perm of perms.permissions) {
+      if (!permissions.includes(perm)) {
+        added.permissions.push(perm);
+        permissions.push(perm);
+      }
+    }
+    for (let origin of perms.origins) {
+      if (!origins.includes(origin)) {
+        added.origins.push(origin);
+        origins.push(origin);
+      }
+    }
+
+    if (added.permissions.length > 0 || added.origins.length > 0) {
+      prefs.saveSoon();
+      // TODO apply the changes
+    }
+  },
+
+  // Revoke permissions from the given extension.  `permissions` is
+  // in the format that is passed to browser.permissions.remove().
+  async remove(extension, perms) {
+    await lazyInit();
+
+    if (!prefs.data[extension.id]) {
+      return;
+    }
+    let {permissions, origins} = prefs.data[extension.id];
+
+    let removed = emptyPermissions();
+
+    for (let perm of perms.permissions) {
+      let i = permissions.indexOf(perm);
+      if (i >= 0) {
+        removed.permissions.push(perm);
+        permissions.splice(i, 1);
+      }
+    }
+    for (let origin of perms.origins) {
+      let i = origins.indexOf(origin);
+      if (i >= 0) {
+        removed.origins.push(origin);
+        origins.splice(i, 1);
+      }
+    }
+
+    if (removed.permissions.length > 0 || removed.origins.length > 0) {
+      prefs.saveSoon();
+      // TODO apply the changes
+    }
+  },
+
+  async removeAll(extension) {
+    await lazyInit();
+    delete prefs.data[extension.id];
+    prefs.saveSoon();
+  },
+};
--- a/toolkit/components/extensions/ExtensionUtils.jsm
+++ b/toolkit/components/extensions/ExtensionUtils.jsm
@@ -1323,17 +1323,41 @@ class MessageManagerProxy {
   handleEvent(event) {
     if (event.type == "SwapDocShells") {
       this.removeListeners(this.eventTarget);
       this.addListeners(event.detail);
     }
   }
 }
 
+/**
+ * Classify an individual permission from a webextension manifest
+ * as a host/origin permission, an api permission, or a regular permission.
+ *
+ * @param {string} perm  The permission string to classify
+ *
+ * @returns {object}
+ *          An object with exactly one of the following properties:
+ *          "origin" to indicate this is a host/origin permission.
+ *          "api" to indicate this is an api permission
+ *                (as used for webextensions experiments).
+ *          "permission" to indicate this is a regular permission.
+ */
+function classifyPermission(perm) {
+  let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
+  if (!match) {
+    return {origin: perm};
+  } else if (match[1] == "experiments" && match[2]) {
+    return {api: match[2]};
+  }
+  return {permission: perm};
+}
+
 this.ExtensionUtils = {
+  classifyPermission,
   defineLazyGetter,
   detectLanguage,
   extend,
   findPathInObject,
   flushJarCache,
   getConsole,
   getInnerWindowID,
   getMessageManager,
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-c-permissions.js
@@ -0,0 +1,19 @@
+"use strict";
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+const {ExtensionError} = ExtensionUtils;
+
+extensions.registerSchemaAPI("permissions", "addon_child", context => {
+  return {
+    permissions: {
+      async request(perms) {
+        let winUtils = context.contentWindow.getInterface(Ci.nsIDOMWindowUtils);
+        if (!winUtils.isHandlingUserInput) {
+          throw new ExtensionError("May only request permissions from a user input handler");
+        }
+
+        return context.childManager.callParentAsyncFunction("permissions.request_parent", [perms]);
+      },
+    },
+  };
+});
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-permissions.js
@@ -0,0 +1,97 @@
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/ExtensionUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionPermissions",
+                                  "resource://gre/modules/ExtensionPermissions.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+                                  "resource://gre/modules/NetUtil.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+                                  "resource://gre/modules/Services.jsm");
+
+const {
+  ExtensionError,
+} = ExtensionUtils;
+
+XPCOMUtils.defineLazyPreferenceGetter(this, "promptsEnabled",
+                                      "extensions.webextOptionalPermissionPrompts");
+
+extensions.registerSchemaAPI("permission", "addon_parent", context => {
+  return {
+    permissions: {
+      async request_parent(perms) {
+        let {permissions, origins} = perms;
+
+        let manifestPermissions = context.extension.manifest.optional_permissions;
+        for (let perm of permissions) {
+          if (!manifestPermissions.includes(perm)) {
+            throw new ExtensionError(`Cannot request permission ${perm} since it was not declared in optional_permissions`);
+          }
+        }
+
+        let optionalOrigins = context.extension.optionalOrigins;
+        for (let origin of origins) {
+          if (!optionalOrigins.subsumes(origin)) {
+            throw new ExtensionError(`Cannot request origin permission for ${origin} since it was not declared in optional_permissions`);
+          }
+        }
+
+        if (promptsEnabled) {
+          let allow = await new Promise(resolve => {
+            let subject = {
+              wrappedJSObject: {
+                browser: context.xulBrowser,
+                name: context.extension.name,
+                icon: context.extension.iconURL,
+                permissions: {permissions, origins},
+                resolve,
+              },
+            };
+            Services.obs.notifyObservers(subject, "webextension-optional-permission-prompt", null);
+          });
+          if (!allow) {
+            return false;
+          }
+        }
+
+        await ExtensionPermissions.add(context.extension, perms);
+        return true;
+      },
+
+      async getAll() {
+        let perms = context.extension.userPermissions;
+        delete perms.apis;
+        return perms;
+      },
+
+      async contains(permissions) {
+        for (let perm of permissions.permissions) {
+          if (!context.extension.hasPermission(perm)) {
+            return false;
+          }
+        }
+
+        for (let origin of permissions.origins) {
+          if (!context.extension.whiteListedHosts.subsumes(origin)) {
+            return false;
+          }
+        }
+
+        return true;
+      },
+
+      async remove(permissions) {
+        await ExtensionPermissions.remove(context.extension, permissions);
+        return true;
+      },
+    },
+  };
+});
+
+/* eslint-disable mozilla/balanced-listeners */
+extensions.on("uninstall", extension => {
+  ExtensionPermissions.removeAll(extension);
+});
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -6,28 +6,30 @@ category webextension-scripts cookies ch
 category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
 category webextension-scripts extension chrome://extensions/content/ext-extension.js
 category webextension-scripts geolocation chrome://extensions/content/ext-geolocation.js
 category webextension-scripts handlers chrome://extensions/content/ext-protocolHandlers.js
 category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts idle chrome://extensions/content/ext-idle.js
 category webextension-scripts management chrome://extensions/content/ext-management.js
 category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
+category webextension-scripts permissions chrome://extensions/content/ext-permissions.js
 category webextension-scripts privacy chrome://extensions/content/ext-privacy.js
 category webextension-scripts proxy chrome://extensions/content/ext-proxy.js
 category webextension-scripts runtime chrome://extensions/content/ext-runtime.js
 category webextension-scripts storage chrome://extensions/content/ext-storage.js
 category webextension-scripts theme chrome://extensions/content/ext-theme.js
 category webextension-scripts topSites chrome://extensions/content/ext-topSites.js
 category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
 category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
 
 # scripts specific for content process.
 category webextension-scripts-content extension chrome://extensions/content/ext-c-extension.js
 category webextension-scripts-content i18n chrome://extensions/content/ext-i18n.js
+category webextension-scripts-content permissions chrome://extensions/content/ext-c-permissions.js
 category webextension-scripts-content runtime chrome://extensions/content/ext-c-runtime.js
 category webextension-scripts-content storage chrome://extensions/content/ext-c-storage.js
 category webextension-scripts-content test chrome://extensions/content/ext-c-test.js
 
 # scripts specific for devtools extension contexts.
 category webextension-scripts-devtools extension chrome://extensions/content/ext-c-extension.js
 category webextension-scripts-devtools i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts-devtools runtime chrome://extensions/content/ext-c-runtime.js
@@ -36,16 +38,17 @@ category webextension-scripts-devtools t
 
 # scripts that must run in the same process as addon code.
 category webextension-scripts-addon backgroundPage chrome://extensions/content/ext-c-backgroundPage.js
 category webextension-scripts-addon extension chrome://extensions/content/ext-c-extension.js
 category webextension-scripts-addon i18n chrome://extensions/content/ext-i18n.js
 #ifndef ANDROID
 category webextension-scripts-addon identity chrome://extensions/content/ext-c-identity.js
 #endif
+category webextension-scripts-addon permissions chrome://extensions/content/ext-c-permissions.js
 category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
 category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
 category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
 
 # schemas
 category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
 category webextension-schemas contextualIdentities chrome://extensions/content/schemas/contextual_identities.json
 category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
@@ -57,16 +60,17 @@ category webextension-schemas handlers c
 category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
 #ifndef ANDROID
 category webextension-schemas identity chrome://extensions/content/schemas/identity.json
 #endif
 category webextension-schemas idle chrome://extensions/content/schemas/idle.json
 category webextension-schemas management chrome://extensions/content/schemas/management.json
 category webextension-schemas native_host_manifest chrome://extensions/content/schemas/native_host_manifest.json
 category webextension-schemas notifications chrome://extensions/content/schemas/notifications.json
+category webextension-schemas permissions chrome://extensions/content/schemas/permissions.json
 category webextension-schemas privacy chrome://extensions/content/schemas/privacy.json
 category webextension-schemas proxy chrome://extensions/content/schemas/proxy.json
 category webextension-schemas runtime chrome://extensions/content/schemas/runtime.json
 category webextension-schemas storage chrome://extensions/content/schemas/storage.json
 category webextension-schemas test chrome://extensions/content/schemas/test.json
 category webextension-schemas theme chrome://extensions/content/schemas/theme.json
 category webextension-schemas top_sites chrome://extensions/content/schemas/top_sites.json
 category webextension-schemas types chrome://extensions/content/schemas/types.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -11,27 +11,29 @@ toolkit.jar:
     content/extensions/ext-cookies.js
     content/extensions/ext-downloads.js
     content/extensions/ext-extension.js
     content/extensions/ext-geolocation.js
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-management.js
     content/extensions/ext-notifications.js
+    content/extensions/ext-permissions.js
     content/extensions/ext-privacy.js
     content/extensions/ext-protocolHandlers.js
     content/extensions/ext-proxy.js
     content/extensions/ext-runtime.js
     content/extensions/ext-storage.js
     content/extensions/ext-theme.js
     content/extensions/ext-topSites.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
     # Below is a separate group using the naming convention ext-c-*.js that run
     # in the child process.
     content/extensions/ext-c-backgroundPage.js
     content/extensions/ext-c-extension.js
 #ifndef ANDROID
     content/extensions/ext-c-identity.js
 #endif
+    content/extensions/ext-c-permissions.js
     content/extensions/ext-c-runtime.js
     content/extensions/ext-c-storage.js
     content/extensions/ext-c-test.js
--- a/toolkit/components/extensions/moz.build
+++ b/toolkit/components/extensions/moz.build
@@ -7,16 +7,17 @@
 EXTRA_JS_MODULES += [
     'Extension.jsm',
     'ExtensionAPI.jsm',
     'ExtensionChild.jsm',
     'ExtensionCommon.jsm',
     'ExtensionContent.jsm',
     'ExtensionManagement.jsm',
     'ExtensionParent.jsm',
+    'ExtensionPermissions.jsm',
     'ExtensionPreferencesManager.jsm',
     'ExtensionSettingsStore.jsm',
     'ExtensionStorage.jsm',
     'ExtensionStorageSync.jsm',
     'ExtensionTabs.jsm',
     'ExtensionUtils.jsm',
     'LegacyExtensionsUtils.jsm',
     'MessageChannel.jsm',
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -17,16 +17,17 @@ toolkit.jar:
 #ifndef ANDROID
     content/extensions/schemas/identity.json
 #endif
     content/extensions/schemas/idle.json
     content/extensions/schemas/management.json
     content/extensions/schemas/manifest.json
     content/extensions/schemas/native_host_manifest.json
     content/extensions/schemas/notifications.json
+    content/extensions/schemas/permissions.json
     content/extensions/schemas/proxy.json
     content/extensions/schemas/privacy.json
     content/extensions/schemas/runtime.json
     content/extensions/schemas/storage.json
     content/extensions/schemas/test.json
     content/extensions/schemas/theme.json
     content/extensions/schemas/top_sites.json
     content/extensions/schemas/types.json
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/permissions.json
@@ -0,0 +1,153 @@
+[
+  {
+    "namespace": "permissions",
+    "permissions": ["manifest:optional_permissions"],
+    "types": [
+      {
+        "id": "Permissions",
+        "type": "object",
+        "properties": {
+          "permissions": {
+            "type": "array",
+            "items": { "$ref": "manifest.OptionalPermission" },
+            "optional": true,
+            "default": []
+          },
+          "origins": {
+            "type": "array",
+            "items": { "$ref": "manifest.MatchPattern" },
+            "optional": true,
+            "default": []
+          }
+        }
+      },
+      {
+        "id": "AnyPermissions",
+        "type": "object",
+        "properties": {
+          "permissions": {
+            "type": "array",
+            "items": { "$ref": "manifest.Permission" },
+            "optional": true,
+            "default": []
+          },
+          "origins": {
+            "type": "array",
+            "items": { "$ref": "manifest.MatchPattern" },
+            "optional": true,
+            "default": []
+          }
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "getAll",
+        "type": "function",
+        "async": "callback",
+        "description": "Get a list of all the extension's permissions.",
+        "parameters": [
+          {
+            "name": "callback",
+            "type": "function",
+            "parameters": [
+              {
+                "name": "permissions",
+                "$ref": "AnyPermissions"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "contains",
+        "type": "function",
+        "async": "callback",
+        "description": "Check if the extension has the given permissions.",
+        "parameters": [
+          {
+            "name": "permissions",
+            "$ref": "AnyPermissions"
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "parameters": [
+              {
+                "name": "result",
+                "type": "boolean"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "request",
+        "type": "function",
+        "allowedContexts": ["content"],
+        "async": "callback",
+        "description": "Request the given permissions.",
+        "parameters": [
+          {
+            "name": "permissions",
+            "$ref": "Permissions"
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "parameters": [
+              {
+                "name": "granted",
+                "type": "boolean"
+              }
+            ]
+          }
+        ]
+      },
+      {
+        "name": "remove",
+        "type": "function",
+        "async": "callback",
+        "description": "Relinquish the given permissions.",
+        "parameters": [
+          {
+            "name": "permissions",
+            "$ref": "Permissions"
+          },
+          {
+            "name": "callback",
+            "type": "function",
+            "parameters": [
+            ]
+          }
+        ]
+      }
+    ],
+    "events": [
+      {
+        "name": "onAdded",
+        "type": "function",
+        "unsupported": true,
+        "description": "Fired when the extension acquires new permissions.",
+        "parameters": [
+          {
+            "name": "permissions",
+            "$ref": "Permissions"
+          }
+        ]
+      },
+      {
+        "name": "onRemoved",
+        "type": "function",
+        "unsupported": true,
+        "description": "Fired when permissions are removed from the extension.",
+        "parameters": [
+          {
+            "name": "permissions",
+            "$ref": "Permissions"
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -68,16 +68,20 @@ let expectedBackgroundApis = [
   "extensionTypes.CSSOrigin",
   "extensionTypes.ImageFormat",
   "extensionTypes.RunAt",
   "management.ExtensionDisabledReason",
   "management.ExtensionInstallType",
   "management.ExtensionType",
   "management.getSelf",
   "management.uninstallSelf",
+  "permissions.getAll",
+  "permissions.contains",
+  "permissions.request",
+  "permissions.remove",
   "runtime.getBackgroundPage",
   "runtime.getBrowserInfo",
   "runtime.getPlatformInfo",
   "runtime.onConnectExternal",
   "runtime.onInstalled",
   "runtime.onMessageExternal",
   "runtime.onStartup",
   "runtime.onUpdateAvailable",
--- a/toolkit/modules/addons/MatchPattern.jsm
+++ b/toolkit/modules/addons/MatchPattern.jsm
@@ -16,16 +16,22 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 
 this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"];
 
 /* globals MatchPattern, MatchGlobs */
 
 const PERMITTED_SCHEMES = ["http", "https", "file", "ftp", "data"];
 const PERMITTED_SCHEMES_REGEXP = PERMITTED_SCHEMES.join("|");
 
+// The basic RE for matching patterns
+const PATTERN_REGEXP = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
+
+// The schemes/protocols implied by a pattern that starts with *://
+const WILDCARD_SCHEMES = ["http", "https"];
+
 // This function converts a glob pattern (containing * and possibly ?
 // as wildcards) to a regular expression.
 function globToRegexp(pat, allowQuestion) {
   // Escape everything except ? and *.
   pat = pat.replace(/[.+^${}()|[\]\\]/g, "\\$&");
 
   if (allowQuestion) {
     pat = pat.replace(/\?/g, ".");
@@ -42,26 +48,25 @@ function SingleMatchPattern(pat) {
   this.pat = pat;
   if (pat == "<all_urls>") {
     this.schemes = PERMITTED_SCHEMES;
     this.hostMatch = () => true;
     this.pathMatch = () => true;
   } else if (!pat) {
     this.schemes = [];
   } else {
-    let re = new RegExp(`^(${PERMITTED_SCHEMES_REGEXP}|\\*)://(\\*|\\*\\.[^*/]+|[^*/]+|)(/.*)$`);
-    let match = re.exec(pat);
+    let match = PATTERN_REGEXP.exec(pat);
     if (!match) {
       Cu.reportError(`Invalid match pattern: '${pat}'`);
       this.schemes = [];
       return;
     }
 
     if (match[1] == "*") {
-      this.schemes = ["http", "https"];
+      this.schemes = WILDCARD_SCHEMES;
     } else {
       this.schemes = [match[1]];
     }
 
     // We allow the host to be empty for file URLs.
     if (match[2] == "" && this.schemes[0] != "file") {
       Cu.reportError(`Invalid match pattern: '${pat}'`);
       this.schemes = [];
@@ -169,16 +174,33 @@ MatchPattern.prototype = {
           return true;
         }
       }
     }
 
     return false;
   },
 
+  // Test if this MatchPattern subsumes the given pattern (i.e., whether
+  // this pattern matches everything the given pattern does).
+  // Note, this method considers only to protocols and hosts/domains,
+  // paths are ignored.
+  subsumes(pattern) {
+    let match = PATTERN_REGEXP.exec(pattern);
+    if (!match) {
+      throw new Error("Invalid match pattern");
+    }
+
+    if (match[1] == "*") {
+      return WILDCARD_SCHEMES.every(scheme => this.matchesIgnoringPath({scheme, host: match[2]}));
+    }
+
+    return this.matchesIgnoringPath({scheme: match[1], host: match[2]});
+  },
+
   serialize() {
     return this.pat;
   },
 };
 
 // Globs can match everything. Be careful, this DOES NOT filter by allowed schemes!
 this.MatchGlobs = function(globs) {
   this.original = globs;