Bug 1640931 - Allow policy to force addons in private browsing. a=dmeehan

Original Revision: https://phabricator.services.mozilla.com/D228607

Differential Revision: https://phabricator.services.mozilla.com/D238839
This commit is contained in:
Mike Kaply 2025-02-20 13:16:27 +00:00
parent e942f3efe9
commit 37a3ea4998
8 changed files with 367 additions and 6 deletions

View file

@ -710,6 +710,9 @@
},
"temporarily_allow_weak_signatures": {
"type": "boolean"
},
"private_browsing": {
"type": "boolean"
}
}
}

View file

@ -2,6 +2,10 @@
* http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
const { ExtensionPermissions } = ChromeUtils.importESModule(
"resource://gre/modules/ExtensionPermissions.sys.mjs"
);
const ADDON_ID = "policytest@mozilla.com";
const BASE_URL =
"http://mochi.test:8888/browser/browser/components/enterprisepolicies/tests/browser";
@ -18,6 +22,59 @@ async function isExtensionLockedAndUpdateDisabled(win, addonID) {
is(updateRow.hidden, true, "Update row should be hidden");
}
add_task(async function test_addon_private_browser_access_locked() {
async function installWithExtensionSettings(extensionSettings = {}) {
let installPromise = waitForAddonInstall(ADDON_ID);
await setupPolicyEngineWithJson({
policies: {
ExtensionSettings: {
"policytest@mozilla.com": {
install_url: `${BASE_URL}/policytest_v0.1.xpi`,
installation_mode: "force_installed",
updates_disabled: true,
...extensionSettings,
},
},
},
});
await installPromise;
let addon = await AddonManager.getAddonByID(ADDON_ID);
isnot(addon, null, "Addon not installed.");
is(addon.version, "0.1", "Addon version is correct");
return addon;
}
let addon = await installWithExtensionSettings();
is(
Boolean(
addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
),
true,
"Addon should be able to change private browsing setting (not set in policy)."
);
await addon.uninstall();
addon = await installWithExtensionSettings({ private_browsing: true });
is(
Boolean(
addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
),
false,
"Addon should NOT be able to change private browsing setting (set to true in policy)."
);
await addon.uninstall();
addon = await installWithExtensionSettings({ private_browsing: false });
is(
Boolean(
addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
),
false,
"Addon should NOT be able to change private browsing setting (set to false in policy)."
);
await addon.uninstall();
});
add_task(async function test_addon_install() {
let installPromise = waitForAddonInstall(ADDON_ID);
await setupPolicyEngineWithJson({

View file

@ -0,0 +1,7 @@
"use strict";
module.exports = {
env: {
webextensions: true,
},
};

View file

@ -15,6 +15,7 @@ const { ExtensionTestUtils } = ChromeUtils.importESModule(
AddonTestUtils.init(this);
AddonTestUtils.overrideCertDB();
AddonTestUtils.appInfo = getAppInfo();
AddonTestUtils.usePrivilegedSignatures = false;
ExtensionTestUtils.init(this);
const server = AddonTestUtils.createHttpServer({ hosts: ["example.com"] });
@ -555,7 +556,7 @@ add_task(async function test_policy_installed_only_addon() {
// Setting back the extension settings with installation_mode set to force_installed
// will install the extension again, and so we need to wait for that and uninstall
// it first (otherwise the addon may endup being installed when the test task is
// completed and trigger an intermittent failre).
// completed and trigger an intermittent failure).
installPromise = waitForAddonInstall(policyOnlyID);
await setupPolicyEngineWithJson({
policies: {
@ -613,3 +614,103 @@ add_task(async function test_policy_installed_only_addon() {
"Got the expect error logged on installing enterprise only extension"
);
});
add_task(async function test_private_browsing() {
async function assertPrivateBrowsingAccess({
addonId,
extensionSettings,
extensionManifest = {},
expectedPrivateBrowsingAccess,
message,
}) {
await setupPolicyEngineWithJson({
policies: {
ExtensionSettings: {
[addonId]: { ...extensionSettings },
},
},
});
let ext = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: {
gecko: { id: addonId },
},
...extensionManifest,
},
useAddonManager: "temporary",
background() {
browser.test.onMessage.addListener(async msg => {
switch (msg) {
case "checkPrivateBrowsing": {
let isAllowed =
await browser.extension.isAllowedIncognitoAccess();
browser.test.sendMessage("privateBrowsing", isAllowed);
break;
}
}
});
},
});
await ext.startup();
ext.sendMessage("checkPrivateBrowsing");
let isAllowedIncognitoAccess = await ext.awaitMessage("privateBrowsing");
Assert.equal(
isAllowedIncognitoAccess,
expectedPrivateBrowsingAccess,
message
);
let addon = await AddonManager.getAddonByID(ext.id);
// Sanity check (to ensure the test extension used in this test are not privileged).
ok(!addon.isPrivileged, "Addon should not be privileged");
let expectLocked = typeof extensionSettings?.private_browsing === "boolean";
Assert.equal(
!(
addon.permissions & AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS
),
expectLocked,
`Addon should ${expectLocked ? "NOT" : ""} be able to change private browsing setting.`
);
await ext.unload();
}
await assertPrivateBrowsingAccess({
addonId: "privatebrowsing-granted-nonprivileged@test",
extensionSettings: { private_browsing: true },
expectedPrivateBrowsingAccess: true,
message:
"Should have access to private browsing (set to true in policy extension setting)",
});
await assertPrivateBrowsingAccess({
addonId: "privatebrowsing-revoked-nonprivileged@test",
extensionSettings: { private_browsing: false },
expectedPrivateBrowsingAccess: false,
message:
"Should NOT have access to private browsing (set to false in policy extension setting)",
});
await assertPrivateBrowsingAccess({
addonId: "privatebrowsing-nosetting-nonprivileged@test",
extensionSettings: {},
expectedPrivateBrowsingAccess: false,
message:
"Should NOT have access to private browsing (NOT set in policy extension setting)",
});
await assertPrivateBrowsingAccess({
addonId: "privatebrowsing-notallowed-nonprivileged@test",
extensionSettings: { private_browsing: true },
extensionManifest: {
incognito: "not_allowed",
},
expectedPrivateBrowsingAccess: false,
message:
"incognito 'not_allowed' extensions should NOT have access to private browser",
});
});

View file

@ -3738,7 +3738,8 @@ export class Extension extends ExtensionData {
// We automatically add permissions to system/built-in extensions.
// Extensions expliticy stating not_allowed will never get permission.
let isAllowed = this.permissions.has(PRIVATE_ALLOWED_PERMISSION);
if (this.manifest.incognito === "not_allowed") {
const hasIncognitoNotAllowed = this.manifest.incognito === "not_allowed";
if (hasIncognitoNotAllowed) {
// If an extension previously had permission, but upgrades/downgrades to
// a version that specifies "not_allowed" in manifest, remove the
// permission.
@ -3764,6 +3765,32 @@ export class Extension extends ExtensionData {
this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
}
// On builds where Enterprise Policies are supported, grant or revoke
// the private browsing access for extensions that are not app provided
// (system and builtin add-ons) or hidden.
if (
Services.policies &&
!this.isAppProvided &&
!this.isHidden &&
!hasIncognitoNotAllowed &&
this.type === "extension"
) {
const settings = Services.policies.getExtensionSettings(this.id);
if (settings?.private_browsing) {
lazy.ExtensionPermissions.add(this.id, {
permissions: [PRIVATE_ALLOWED_PERMISSION],
origins: [],
});
this.permissions.add(PRIVATE_ALLOWED_PERMISSION);
} else if (settings?.private_browsing === false) {
lazy.ExtensionPermissions.remove(this.id, {
permissions: [PRIVATE_ALLOWED_PERMISSION],
origins: [],
});
this.permissions.delete(PRIVATE_ALLOWED_PERMISSION);
}
}
// We only want to update the SVG_CONTEXT_PROPERTIES_PERMISSION during
// install and upgrade/downgrade startups.
if (INSTALL_AND_UPDATE_STARTUP_REASONS.has(this.startupReason)) {

View file

@ -910,9 +910,12 @@ export class AddonInternal {
}
}
let settings = Services.policies?.getExtensionSettings(this.id) || {};
// The permission to "toggle the private browsing access" is locked down
// when the extension has opted out or it gets the permission automatically
// on every extension startup (as system, privileged and builtin addons).
// on every extension startup (as system, privileged and builtin addons) or
// when private browsing access as been set and locked through enterprise
// policy settings.
if (
(this.type === "extension" ||
// TODO(Bug 1789718): Remove after the deprecated XPIProvider-based implementation is also removed.
@ -920,7 +923,8 @@ export class AddonInternal {
this.incognito !== "not_allowed" &&
this.signedState !== lazy.AddonManager.SIGNEDSTATE_PRIVILEGED &&
this.signedState !== lazy.AddonManager.SIGNEDSTATE_SYSTEM &&
!this.location.isBuiltin
!this.location.isBuiltin &&
!("private_browsing" in settings)
) {
permissions |= lazy.AddonManager.PERM_CAN_CHANGE_PRIVATEBROWSING_ACCESS;
}

View file

@ -11,9 +11,10 @@ AddonTestUtils.createAppInfo(
"1"
);
async function getWrapper(id, hidden) {
async function getWrapper(id, hidden, { manifest } = {}) {
let wrapper = await installBuiltinExtension({
manifest: {
...manifest,
browser_specific_settings: { gecko: { id } },
hidden,
},
@ -147,3 +148,79 @@ add_task(async function test_builtin_update() {
await wrapper.unload();
await AddonTestUtils.promiseShutdownManager();
});
// Tests private_browsing policy extension settings doesn't apply
// to built-in extensions.
add_task(
// This test is skipped on builds that don't support enterprise policies
// (e.g. android builds).
{ skip_if: () => !Services.policies },
async function test_builtin_private_browsing_policy_setting() {
await AddonTestUtils.promiseStartupManager();
const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
);
let id = "builtin-addon@tests.mozilla.org";
let idNotAllowed = "builtin-not-allowed@tests.mozilla.org";
await EnterprisePolicyTesting.setupPolicyEngineWithJson({
policies: {
ExtensionSettings: {
[id]: {
private_browsing: false,
},
[idNotAllowed]: {
private_browsing: true,
},
},
},
});
// Sanity check the policy data has been loaded.
Assert.equal(
Services.policies.getExtensionSettings(id)?.private_browsing,
false,
`Got expected policies setting private_browsing for ${id}`
);
Assert.equal(
Services.policies.getExtensionSettings(idNotAllowed)?.private_browsing,
true,
`Got expected policies setting private_browsing for ${idNotAllowed}`
);
async function assertPrivateBrowsingAllowed({
addonId,
manifest,
expectedPrivateBrowsingAllowed,
}) {
let wrapper = await getWrapper(addonId, undefined, { manifest });
let addon = await promiseAddonByID(addonId);
notEqual(addon, null, `${addonId} is installed`);
ok(addon.isActive, `${addonId} is active`);
ok(addon.isPrivileged, `${addonId} is privileged`);
ok(wrapper.extension.isAppProvided, `${addonId} is app provided`);
equal(
wrapper.extension.privateBrowsingAllowed,
expectedPrivateBrowsingAllowed,
`Builtin Addon ${addonId} should ${expectedPrivateBrowsingAllowed ? "" : "NOT"} have access private browsing access`
);
await wrapper.unload();
}
await assertPrivateBrowsingAllowed({
addonId: id,
expectedPrivateBrowsingAllowed: true,
});
await assertPrivateBrowsingAllowed({
addonId: idNotAllowed,
manifest: {
incognito: "not_allowed",
},
expectedPrivateBrowsingAllowed: false,
});
await AddonTestUtils.promiseShutdownManager();
}
);

View file

@ -13,9 +13,14 @@ AddonTestUtils.createAppInfo(
AddonTestUtils.usePrivilegedSignatures = id => id.startsWith("system");
async function promiseInstallSystemProfileExtension(id, hidden) {
async function promiseInstallSystemProfileExtension(
id,
hidden,
{ manifest } = {}
) {
let xpi = await AddonTestUtils.createTempWebExtensionFile({
manifest: {
...manifest,
browser_specific_settings: { gecko: { id } },
hidden,
},
@ -202,3 +207,83 @@ add_task(async function test_system_profile_location_require_system_cert() {
await addon.uninstall();
await AddonTestUtils.promiseShutdownManager();
});
// Tests private_browsing policy extension settings doesn't apply
// to system addons.
add_task(
// This test is skipped on builds that don't support enterprise policies
// (e.g. android builds).
{ skip_if: () => !Services.policies },
async function test_builtin_private_browsing_policy_setting() {
await AddonTestUtils.promiseStartupManager();
const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
"resource://testing-common/EnterprisePolicyTesting.sys.mjs"
);
let id = "systemaddon@tests.mozilla.org";
let idNotAllowed = "systemaddon-not-allowed@tests.mozilla.org";
await EnterprisePolicyTesting.setupPolicyEngineWithJson({
policies: {
ExtensionSettings: {
[id]: {
private_browsing: false,
},
[idNotAllowed]: {
private_browsing: true,
},
},
},
});
// Sanity check the policy data has been loaded.
Assert.equal(
Services.policies.getExtensionSettings(id)?.private_browsing,
false,
`Got expected policies setting private_browsing for ${id}`
);
Assert.equal(
Services.policies.getExtensionSettings(idNotAllowed)?.private_browsing,
true,
`Got expected policies setting private_browsing for ${idNotAllowed}`
);
async function assertPrivateBrowsingAllowed({
addonId,
manifest,
expectedPrivateBrowsingAllowed,
}) {
let wrapper = await promiseInstallSystemProfileExtension(
addonId,
undefined,
{ manifest }
);
let addon = await promiseAddonByID(addonId);
notEqual(addon, null, `${addonId} is installed`);
ok(addon.isActive, `${addonId} is active`);
ok(addon.isPrivileged, `${addonId} is privileged`);
ok(wrapper.extension.isAppProvided, `${addonId} is app provided`);
equal(
wrapper.extension.privateBrowsingAllowed,
expectedPrivateBrowsingAllowed,
`Builtin Addon ${addonId} should ${expectedPrivateBrowsingAllowed ? "" : "NOT"} have access private browsing access`
);
await wrapper.unload();
}
await assertPrivateBrowsingAllowed({
addonId: id,
expectedPrivateBrowsingAllowed: true,
});
await assertPrivateBrowsingAllowed({
addonId: idNotAllowed,
manifest: {
incognito: "not_allowed",
},
expectedPrivateBrowsingAllowed: false,
});
await AddonTestUtils.promiseShutdownManager();
}
);