From 1b3c71a54f91f0c8a0d1f3ce429a7b9c7bd32414 Mon Sep 17 00:00:00 2001 From: Andrew Swan Date: Thu, 23 Mar 2017 21:48:03 -0700 Subject: [PATCH] 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 --HG-- extra : rebase_source : f623b4c7c66888ab1fb4876a3d63ec47677711b8 extra : source : e4c13d11e401ae3bd40be3a5a7fb0aaf95c992bb --- .../en-US/chrome/browser/browser.properties | 11 ++ browser/modules/ExtensionsUI.jsm | 9 ++ toolkit/components/extensions/Extension.jsm | 19 ++- .../extensions/ExtensionPermissions.jsm | 111 +++++++++++++ .../components/extensions/ExtensionUtils.jsm | 24 +++ .../extensions/ext-c-permissions.js | 19 +++ .../components/extensions/ext-permissions.js | 97 +++++++++++ .../extensions/extensions-toolkit.manifest | 4 + toolkit/components/extensions/jar.mn | 2 + toolkit/components/extensions/moz.build | 1 + toolkit/components/extensions/schemas/jar.mn | 1 + .../extensions/schemas/permissions.json | 153 ++++++++++++++++++ toolkit/modules/addons/MatchPattern.jsm | 28 +++- 13 files changed, 471 insertions(+), 8 deletions(-) create mode 100644 toolkit/components/extensions/ExtensionPermissions.jsm create mode 100644 toolkit/components/extensions/ext-c-permissions.js create mode 100644 toolkit/components/extensions/ext-permissions.js create mode 100644 toolkit/components/extensions/schemas/permissions.json diff --git a/browser/locales/en-US/chrome/browser/browser.properties b/browser/locales/en-US/chrome/browser/browser.properties index 0f0fe6f4371c..902b1e06e93b 100644 --- a/browser/locales/en-US/chrome/browser/browser.properties +++ b/browser/locales/en-US/chrome/browser/browser.properties @@ -84,6 +84,17 @@ webextPerms.updateText=%S has been updated. You must approve new permissions bef 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 diff --git a/browser/modules/ExtensionsUI.jsm b/browser/modules/ExtensionsUI.jsm index 0322679178ad..21ad6009aa6e 100644 --- a/browser/modules/ExtensionsUI.jsm +++ b/browser/modules/ExtensionsUI.jsm @@ -41,6 +41,7 @@ this.ExtensionsUI = { 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(); }, @@ -203,6 +204,14 @@ this.ExtensionsUI = { 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)); } }, diff --git a/toolkit/components/extensions/Extension.jsm b/toolkit/components/extensions/Extension.jsm index 3d4cebe0d137..471024f1e712 100644 --- a/toolkit/components/extensions/Extension.jsm +++ b/toolkit/components/extensions/Extension.jsm @@ -81,6 +81,7 @@ var { } = ExtensionParent; const { + classifyPermission, EventEmitter, LocaleData, StartupCache, @@ -482,12 +483,11 @@ this.ExtensionData = class { } 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); @@ -680,6 +680,7 @@ this.Extension = class extends ExtensionData { this.apis = []; this.whiteListedHosts = null; + this._optionalOrigins = null; this.webAccessibleResources = null; this.emitter = new EventEmitter(); @@ -1018,4 +1019,12 @@ this.Extension = class extends ExtensionData { 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; + } }; diff --git a/toolkit/components/extensions/ExtensionPermissions.jsm b/toolkit/components/extensions/ExtensionPermissions.jsm new file mode 100644 index 000000000000..23a60c45b8c3 --- /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(); + }, +}; diff --git a/toolkit/components/extensions/ExtensionUtils.jsm b/toolkit/components/extensions/ExtensionUtils.jsm index 8ed715f142ba..6187621029bf 100644 --- a/toolkit/components/extensions/ExtensionUtils.jsm +++ b/toolkit/components/extensions/ExtensionUtils.jsm @@ -1328,7 +1328,31 @@ class MessageManagerProxy { } } +/** + * 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, diff --git a/toolkit/components/extensions/ext-c-permissions.js b/toolkit/components/extensions/ext-c-permissions.js new file mode 100644 index 000000000000..624f16055755 --- /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]); + }, + }, + }; +}); diff --git a/toolkit/components/extensions/ext-permissions.js b/toolkit/components/extensions/ext-permissions.js new file mode 100644 index 000000000000..2e8fc27bc870 --- /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); +}); diff --git a/toolkit/components/extensions/extensions-toolkit.manifest b/toolkit/components/extensions/extensions-toolkit.manifest index 69ed40dce528..96f0820725b5 100644 --- a/toolkit/components/extensions/extensions-toolkit.manifest +++ b/toolkit/components/extensions/extensions-toolkit.manifest @@ -11,6 +11,7 @@ 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 @@ -23,6 +24,7 @@ category webextension-scripts webRequest chrome://extensions/content/ext-webRequ # 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 @@ -41,6 +43,7 @@ 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 @@ -62,6 +65,7 @@ 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 diff --git a/toolkit/components/extensions/jar.mn b/toolkit/components/extensions/jar.mn index 90def2f610da..81ee0b598cc4 100644 --- a/toolkit/components/extensions/jar.mn +++ b/toolkit/components/extensions/jar.mn @@ -16,6 +16,7 @@ toolkit.jar: 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 @@ -32,6 +33,7 @@ toolkit.jar: #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 diff --git a/toolkit/components/extensions/moz.build b/toolkit/components/extensions/moz.build index de6f4d069ee5..775ce7f16dde 100644 --- a/toolkit/components/extensions/moz.build +++ b/toolkit/components/extensions/moz.build @@ -12,6 +12,7 @@ EXTRA_JS_MODULES += [ 'ExtensionContent.jsm', 'ExtensionManagement.jsm', 'ExtensionParent.jsm', + 'ExtensionPermissions.jsm', 'ExtensionPreferencesManager.jsm', 'ExtensionSettingsStore.jsm', 'ExtensionStorage.jsm', diff --git a/toolkit/components/extensions/schemas/jar.mn b/toolkit/components/extensions/schemas/jar.mn index 069ddf84b27a..8b6809e507f9 100644 --- a/toolkit/components/extensions/schemas/jar.mn +++ b/toolkit/components/extensions/schemas/jar.mn @@ -22,6 +22,7 @@ toolkit.jar: 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 diff --git a/toolkit/components/extensions/schemas/permissions.json b/toolkit/components/extensions/schemas/permissions.json new file mode 100644 index 000000000000..9a289ae9dcf1 --- /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" + } + ] + } + ] + } +] diff --git a/toolkit/modules/addons/MatchPattern.jsm b/toolkit/modules/addons/MatchPattern.jsm index a4ed65c131d0..d70b4b06c9ab 100644 --- a/toolkit/modules/addons/MatchPattern.jsm +++ b/toolkit/modules/addons/MatchPattern.jsm @@ -21,6 +21,12 @@ this.EXPORTED_SYMBOLS = ["MatchPattern", "MatchGlobs", "MatchURLFilters"]; 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) { @@ -47,8 +53,7 @@ function SingleMatchPattern(pat) { } 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 = []; @@ -56,7 +61,7 @@ function SingleMatchPattern(pat) { } if (match[1] == "*") { - this.schemes = ["http", "https"]; + this.schemes = WILDCARD_SCHEMES; } else { this.schemes = [match[1]]; } @@ -174,6 +179,23 @@ MatchPattern.prototype = { 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; },