Bug 1745820 - Add controls for optional origins to about:addons permissions tab r=rpl,robwu

Differential Revision: https://phabricator.services.mozilla.com/D144070
This commit is contained in:
Tomislav Jovanovic 2022-04-26 23:48:44 +00:00
parent 0b6b34fe31
commit c4918e9f66
3 changed files with 295 additions and 46 deletions

View file

@ -1238,7 +1238,7 @@ class ExtensionData {
}
// Unfortunately, we treat <all_urls> as an API permission as well.
if (!type.origin || perm === "<all_urls>") {
if (!type.origin || (perm === "<all_urls>" && !result.originControls)) {
permissions.add(perm);
}
}
@ -1702,38 +1702,68 @@ class ExtensionData {
* Classify host permissions
* @param {array<string>} origins
* permission origins
* @param {boolean} ignoreNonWebSchemes
* return only these schemes: *, http, https, ws, wss
*
* @returns {object}
* "object.allUrls" contains the permission used to obtain all urls access
* "object.wildcards" set contains permissions with wildcards
* "object.sites" set contains explicit host permissions
* @param {string} .allUrls permission used to obtain all urls access
* @param {Set} .wildcards set contains permissions with wildcards
* @param {Set} .sites set contains explicit host permissions
* @param {Map} .wildcardsMap mapping origin wildcards to labels
* @param {Map} .sitesMap mapping origin patterns to labels
*/
static classifyOriginPermissions(origins = []) {
static classifyOriginPermissions(origins = [], ignoreNonWebSchemes = false) {
let allUrls = null,
wildcards = new Set(),
sites = new Set();
sites = new Set(),
// TODO: use map.values() instead of these sets. Note: account for two
// match patterns producing the same permission string, see bug 1765828.
wildcardsMap = new Map(),
sitesMap = new Map();
// https://searchfox.org/mozilla-central/rev/6f6cf28107/toolkit/components/extensions/MatchPattern.cpp#235
const wildcardSchemes = ["*", "http", "https", "ws", "wss"];
for (let permission of origins) {
if (permission == "<all_urls>") {
allUrls = permission;
break;
continue;
}
// Privileged extensions may request access to "about:"-URLs, such as
// about:reader.
let match = /^[a-z*]+:\/\/([^/]*)\/|^about:/.exec(permission);
let match = /^([a-z*]+):\/\/([^/]*)\/|^about:/.exec(permission);
if (!match) {
throw new Error(`Unparseable host permission ${permission}`);
}
// Note: the scheme is ignored in the permission warnings. If this ever
// changes, update the comparePermissions method as needed.
if (!match[1] || match[1] == "*") {
allUrls = permission;
} else if (match[1].startsWith("*.")) {
wildcards.add(match[1].slice(2));
let [, scheme, host] = match;
if (ignoreNonWebSchemes && !wildcardSchemes.includes(scheme)) {
continue;
}
if (!host || host == "*") {
if (!allUrls) {
allUrls = permission;
}
} else if (host.startsWith("*.")) {
wildcards.add(host.slice(2));
// Using MatchPattern to normalize the pattern string.
let pat = new MatchPattern(permission, { ignorePath: true });
wildcardsMap.set(pat.pattern, `${scheme}://${host.slice(2)}`);
} else {
sites.add(match[1]);
sites.add(host);
let pat = new MatchPattern(permission, {
ignorePath: true,
// Safe because used just for normalization, not for granting access.
restrictSchemes: false,
});
sitesMap.set(pat.pattern, `${scheme}://${host}`);
}
}
return { allUrls, wildcards, sites };
return { allUrls, wildcards, sites, wildcardsMap, sitesMap };
}
/**
@ -1764,6 +1794,9 @@ class ExtensionData {
* @param {boolean} options.collapseOrigins
* Wether to limit the number of displayed host permissions.
* Default is false.
* @param {boolean} options.buildOptionalOrigins
* Wether to build optional origins Maps for permission
* controls. Defaults to false.
* @param {function} options.getKeyForPermission
* An optional callback function that returns the locale key for a given
* permission name (set by default to a callback returning the locale
@ -1792,6 +1825,7 @@ class ExtensionData {
bundle,
{
collapseOrigins = false,
buildOptionalOrigins = false,
getKeyForPermission = perm => `webextPerms.description.${perm}`,
} = {}
) {
@ -1955,15 +1989,31 @@ class ExtensionData {
// So if we don't find one then just skip it.
}
}
allUrls = ExtensionData.classifyOriginPermissions(
optional_permissions.origins
).allUrls;
if (allUrls) {
result.optionalOrigins[allUrls] = bundle.GetStringFromName(
let optionalInfo = ExtensionData.classifyOriginPermissions(
optional_permissions.origins,
true
);
if (optionalInfo.allUrls) {
result.optionalOrigins[optionalInfo.allUrls] = bundle.GetStringFromName(
"webextPerms.hostDescription.allUrls"
);
}
// Current UX controls are meant for developer testing with mv3.
if (buildOptionalOrigins) {
for (let [pattern, originLabel] of optionalInfo.wildcardsMap.entries()) {
let key = "webextPerms.hostDescription.wildcard";
let str = bundle.formatStringFromName(key, [originLabel]);
result.optionalOrigins[pattern] = str;
}
for (let [pattern, originLabel] of optionalInfo.sitesMap.entries()) {
let key = "webextPerms.hostDescription.oneSite";
let str = bundle.formatStringFromName(key, [originLabel]);
result.optionalOrigins[pattern] = str;
}
}
if (info.type == "sideload") {
headerKey = "webextPerms.sideloadHeader";
let key = !result.msgs.length

View file

@ -45,6 +45,12 @@ XPCOMUtils.defineLazyGetter(this, "extensionStylesheets", () => {
return ExtensionParent.extensionStylesheets;
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"manifestV3enabled",
"extensions.manifestV3.enabled"
);
XPCOMUtils.defineLazyPreferenceGetter(
this,
"SUPPORT_URL",
@ -2558,19 +2564,35 @@ class AddonPermissionsList extends HTMLElement {
async render() {
let appName = brandBundle.GetStringFromName("brandShortName");
let empty = { origins: [], permissions: [] };
let requiredPerms = { ...(this.addon.userPermissions ?? empty) };
let optionalPerms = { ...(this.addon.optionalPermissions ?? empty) };
let grantedPerms = await ExtensionPermissions.get(this.addon.id);
if (manifestV3enabled) {
// If optional permissions include <all_urls>, extension can request and
// be granted permission for individual sites not listed in the manifest.
// Include them as well in the optional origins list.
optionalPerms.origins = [
...optionalPerms.origins,
...grantedPerms.origins.filter(o => !requiredPerms.origins.includes(o)),
];
}
let permissions = Extension.formatPermissionStrings(
{
permissions: this.addon.userPermissions,
optionalPermissions: this.addon.optionalPermissions,
permissions: requiredPerms,
optionalPermissions: optionalPerms,
appName,
},
browserBundle
browserBundle,
{ buildOptionalOrigins: manifestV3enabled }
);
let optionalEntries = [
...Object.entries(permissions.optionalPermissions),
...Object.entries(permissions.optionalOrigins),
];
let perms = await ExtensionPermissions.get(this.addon.id);
this.textContent = "";
let frag = importTemplate("addon-permissions-list");
@ -2587,6 +2609,7 @@ class AddonPermissionsList extends HTMLElement {
list.appendChild(item);
}
}
if (optionalEntries.length) {
let section = frag.querySelector(".addon-permissions-optional");
section.hidden = false;
@ -2613,7 +2636,10 @@ class AddonPermissionsList extends HTMLElement {
toggle.setAttribute("permission-type", type);
toggle.setAttribute("type", "checkbox");
if (perms.permissions.includes(perm) || perms.origins.includes(perm)) {
if (
grantedPerms.permissions.includes(perm) ||
grantedPerms.origins.includes(perm)
) {
toggle.checked = true;
item.classList.add("permission-checked");
}
@ -3033,11 +3059,12 @@ class AddonCard extends HTMLElement {
}
permissions = [permission];
} else if (type == "origin") {
if (
action == "add" &&
!addon.optionalPermissions.origins.includes(permission)
) {
throw new Error("origin missing from manifest");
if (action === "add") {
let { origins } = addon.optionalPermissions;
let patternSet = new MatchPatternSet(origins, { ignorePath: true });
if (!patternSet.subsumes(new MatchPattern(permission))) {
throw new Error("origin missing from manifest");
}
}
origins = [permission];
} else {

View file

@ -18,26 +18,34 @@ async function background() {
});
}
async function getExtensions() {
async function getExtensions({ manifest_version = 2 } = {}) {
let extensions = {
"addon0@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 0",
applications: { gecko: { id: "addon0@mochi.test" } },
permissions: ["alarms", "contextMenus"],
},
background,
useAddonManager: "temporary",
}),
"addon1@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 1",
applications: { gecko: { id: "addon1@mochi.test" } },
permissions: [
"alarms",
"contextMenus",
"tabs",
"webNavigation",
"<all_urls>",
"file://*/*",
],
permissions: ["alarms", "contextMenus", "tabs", "webNavigation"],
// Note: for easier testing, we merge host_permissions into permissions
// when loading mv2 extensions, see ExtensionTestCommon.generateFiles.
host_permissions: ["<all_urls>", "file://*/*"],
},
background,
useAddonManager: "temporary",
}),
"addon2@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 2",
applications: { gecko: { id: "addon2@mochi.test" } },
permissions: ["alarms", "contextMenus"],
@ -48,6 +56,7 @@ async function getExtensions() {
}),
"addon3@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 3",
version: "1.0",
applications: { gecko: { id: "addon3@mochi.test" } },
@ -59,6 +68,7 @@ async function getExtensions() {
}),
"addon4@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 4",
applications: { gecko: { id: "addon4@mochi.test" } },
optional_permissions: ["tabs", "webNavigation"],
@ -68,6 +78,7 @@ async function getExtensions() {
}),
"addon5@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 5",
applications: { gecko: { id: "addon5@mochi.test" } },
optional_permissions: ["*://*/*"],
@ -75,8 +86,47 @@ async function getExtensions() {
background,
useAddonManager: "temporary",
}),
"priv6@mochi.test": ExtensionTestUtils.loadExtension({
isPrivileged: true,
manifest: {
manifest_version,
name: "Privileged add-on 6",
applications: { gecko: { id: "priv6@mochi.test" } },
optional_permissions: [
"file://*/*",
"about:reader*",
"resource://pdf.js/*",
"*://*.mozilla.com/*",
"*://*/*",
"<all_urls>",
],
},
background,
useAddonManager: "temporary",
}),
"addon7@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 7",
applications: { gecko: { id: "addon7@mochi.test" } },
optional_permissions: ["<all_urls>", "*://*/*", "file://*/*"],
},
background,
useAddonManager: "temporary",
}),
"addon8@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 8",
applications: { gecko: { id: "addon8@mochi.test" } },
optional_permissions: ["*://*/*", "file://*/*", "<all_urls>"],
},
background,
useAddonManager: "temporary",
}),
"other@mochi.test": ExtensionTestUtils.loadExtension({
manifest: {
manifest_version,
name: "Test add-on 6",
applications: { gecko: { id: "other@mochi.test" } },
optional_permissions: [
@ -102,6 +152,8 @@ async function runTest(options) {
permissions = [],
optional_permissions = [],
optional_enabled = [],
// Map<permission->string> to check optional_permissions against, if set.
optional_strings = {},
view,
} = options;
if (extension) {
@ -300,7 +352,14 @@ async function runTest(options) {
// Check the row is a permission row with the correct key on the toggle
// control.
let row = permission_rows.shift();
let toggle = row.firstElementChild.lastElementChild;
let label = row.firstElementChild;
let str = optional_strings[name];
if (str) {
is(label.textContent, str, `Expected permission string ${str}`);
}
let toggle = label.lastElementChild;
ok(
row.classList.contains("permission-info"),
`optional permission row for ${name}`
@ -330,23 +389,44 @@ async function runTest(options) {
}
}
add_task(async function testPermissionsView() {
async function testPermissionsView({ manifestV3enabled, manifest_version }) {
await SpecialPowers.pushPrefEnv({
set: [["extensions.manifestV3.enabled", manifestV3enabled]],
});
// pre-set a permission prior to starting extensions.
await ExtensionPermissions.add("addon4@mochi.test", {
permissions: ["tabs"],
origins: [],
});
let extensions = await getExtensions();
let extensions = await getExtensions({ manifest_version });
info("Check add-on with required permissions");
await runTest({
extension: extensions["addon1@mochi.test"],
permissions: ["<all_urls>", "tabs", "webNavigation"],
});
if (manifest_version < 3) {
await runTest({
extension: extensions["addon1@mochi.test"],
permissions: ["<all_urls>", "tabs", "webNavigation"],
});
} else {
await runTest({
extension: extensions["addon1@mochi.test"],
permissions: ["tabs", "webNavigation"],
optional_permissions: ["<all_urls>"],
});
}
info("Check add-on without any displayable permissions");
await runTest({ extension: extensions["addon2@mochi.test"] });
await runTest({ extension: extensions["addon0@mochi.test"] });
info("Check add-on with only one optional origin.");
await runTest({
extension: extensions["addon2@mochi.test"],
optional_permissions: manifestV3enabled ? ["http://mochi.test/*"] : [],
optional_strings: {
"http://mochi.test/*": "Access your data for http://mochi.test",
},
});
info("Check add-on with both required and optional permissions");
await runTest({
@ -355,6 +435,28 @@ add_task(async function testPermissionsView() {
optional_permissions: ["webNavigation", "<all_urls>"],
});
// Grant a specific optional host permission not listed in the manifest.
await ExtensionPermissions.add("addon3@mochi.test", {
permissions: [],
origins: ["https://example.com/*"],
});
extensions["addon3@mochi.test"].awaitMessage("permission-added");
info("Check addon3 again and expect the new optional host permission");
await runTest({
extension: extensions["addon3@mochi.test"],
permissions: ["tabs"],
optional_permissions: [
"webNavigation",
"<all_urls>",
...(manifestV3enabled ? ["https://example.com/*"] : []),
],
optional_enabled: ["https://example.com/*"],
optional_strings: {
"https://example.com/*": "Access your data for https://example.com",
},
});
info("Check add-on with only optional permissions, tabs is pre-enabled");
await runTest({
extension: extensions["addon4@mochi.test"],
@ -368,9 +470,48 @@ add_task(async function testPermissionsView() {
optional_permissions: ["*://*/*"],
});
info("Check privileged add-on with non-web origin permissions");
await runTest({
extension: extensions["priv6@mochi.test"],
optional_permissions: [
"<all_urls>",
...(manifestV3enabled ? ["*://*.mozilla.com/*"] : []),
],
optional_strings: {
"*://*.mozilla.com/*":
"Access your data for sites in the *://mozilla.com domain",
},
});
info(`Check that <all_urls> is used over other "all websites" permissions`);
await runTest({
extension: extensions["addon7@mochi.test"],
optional_permissions: ["<all_urls>"],
});
info(`And again with permissions in the opposite order in the manifest`);
await runTest({
extension: extensions["addon8@mochi.test"],
optional_permissions: ["<all_urls>"],
});
for (let ext of Object.values(extensions)) {
await ext.unload();
}
await SpecialPowers.popPrefEnv();
}
add_task(async function testPermissionsView_MV2_manifestV3disabled() {
await testPermissionsView({ manifestV3enabled: false, manifest_version: 2 });
});
add_task(async function testPermissionsView_MV2_manifestV3enabled() {
await testPermissionsView({ manifestV3enabled: true, manifest_version: 2 });
});
add_task(async function testPermissionsView_MV3() {
await testPermissionsView({ manifestV3enabled: true, manifest_version: 3 });
});
add_task(async function testPermissionsViewStates() {
@ -445,3 +586,34 @@ add_task(async function testPermissionsViewStates() {
await closeView(view);
await extension.unload();
});
add_task(async function testAllUrlsNotGrantedUnconditionally_MV3() {
await SpecialPowers.pushPrefEnv({
set: [["extensions.manifestV3.enabled", true]],
});
const extension = ExtensionTestUtils.loadExtension({
manifest: {
manifest_version: 3,
host_permissions: ["<all_urls>"],
},
async background() {
const perms = await browser.permissions.getAll();
browser.test.sendMessage("granted-permissions", perms);
},
});
await extension.startup();
const perms = await extension.awaitMessage("granted-permissions");
ok(
!perms.origins.includes("<all_urls>"),
"Optional <all_urls> should not be granted as host permission yet"
);
ok(
!perms.permissions.includes("<all_urls>"),
"Optional <all_urls> should not be granted as an API permission neither"
);
await extension.unload();
await SpecialPowers.popPrefEnv();
});