Bug 1197420 Part 3 Initial browser.permissions api support r=kmag draft
authorAndrew Swan <aswan@mozilla.com>
Fri, 24 Mar 2017 13:55:09 -0700
changeset 538631 d5cc18abbae6809b196f8497ff91608d662d5030
parent 538630 e24e1b52edd3ddcd353a6407497ec4076039af03
child 538632 40f53e5f34f02749a5027aa324cf0843c5d2c837
push id50951
push useraswan@mozilla.com
push dateFri, 24 Mar 2017 21:33:07 +0000
reviewerskmag
bugs1197420
milestone55.0a1
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;