forked from mirrors/gecko-dev
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:
parent
0b6b34fe31
commit
c4918e9f66
3 changed files with 295 additions and 46 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue