summaryrefslogtreecommitdiffstats
path: root/toolkit/components/enterprisepolicies
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/enterprisepolicies')
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePolicies.sys.mjs20
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs22
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs740
-rw-r--r--toolkit/components/enterprisepolicies/WindowsGPOParser.sys.mjs116
-rw-r--r--toolkit/components/enterprisepolicies/components.conf16
-rw-r--r--toolkit/components/enterprisepolicies/macOSPoliciesParser.sys.mjs158
-rw-r--r--toolkit/components/enterprisepolicies/moz.build37
-rw-r--r--toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl76
-rw-r--r--toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs166
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser.toml14
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js140
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js14
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js70
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_gpo.js206
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_mistyped_json.js17
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/config_broken_json.json3
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/head.js24
-rw-r--r--toolkit/components/enterprisepolicies/tests/moz.build15
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/head.js36
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/test_empty.js16
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.toml6
21 files changed, 1912 insertions, 0 deletions
diff --git a/toolkit/components/enterprisepolicies/EnterprisePolicies.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePolicies.sys.mjs
new file mode 100644
index 0000000000..7bb3a53a5d
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePolicies.sys.mjs
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export function EnterprisePolicies() {
+ // eslint-disable-next-line mozilla/use-services
+ const appinfo = Cc["@mozilla.org/xre/app-info;1"].getService(
+ Ci.nsIXULRuntime
+ );
+ if (appinfo.processType == appinfo.PROCESS_TYPE_DEFAULT) {
+ const { EnterprisePoliciesManager } = ChromeUtils.importESModule(
+ "resource://gre/modules/EnterprisePoliciesParent.sys.mjs"
+ );
+ return new EnterprisePoliciesManager();
+ }
+ const { EnterprisePoliciesManagerContent } = ChromeUtils.importESModule(
+ "resource://gre/modules/EnterprisePoliciesContent.sys.mjs"
+ );
+ return new EnterprisePoliciesManagerContent();
+}
diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs
new file mode 100644
index 0000000000..d9f8c5adb3
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.sys.mjs
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+export class EnterprisePoliciesManagerContent {
+ get status() {
+ return (
+ Services.cpmm.sharedData.get("EnterprisePolicies:Status") ||
+ Ci.nsIEnterprisePolicies.INACTIVE
+ );
+ }
+
+ isAllowed(feature) {
+ let disallowedFeatures = Services.cpmm.sharedData.get(
+ "EnterprisePolicies:DisallowedFeatures"
+ );
+ return !(disallowedFeatures && disallowedFeatures.has(feature));
+ }
+}
+
+EnterprisePoliciesManagerContent.prototype.QueryInterface =
+ ChromeUtils.generateQI(["nsIEnterprisePolicies"]);
diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
new file mode 100644
index 0000000000..15fa2193c6
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs
@@ -0,0 +1,740 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ Policies: "resource:///modules/policies/Policies.sys.mjs",
+ WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.sys.mjs",
+ macOSPoliciesParser:
+ "resource://gre/modules/policies/macOSPoliciesParser.sys.mjs",
+});
+
+// This is the file that will be searched for in the
+// ${InstallDir}/distribution folder.
+const POLICIES_FILENAME = "policies.json";
+
+// When true browser policy is loaded per-user from
+// /run/user/$UID/appname
+const PREF_PER_USER_DIR = "toolkit.policies.perUserDir";
+// For easy testing, modify the helpers/sample.json file,
+// and set PREF_ALTERNATE_PATH in firefox.js as:
+// /your/repo/browser/components/enterprisepolicies/helpers/sample.json
+const PREF_ALTERNATE_PATH = "browser.policies.alternatePath";
+// For testing GPO, you can set an alternate location in testing
+const PREF_ALTERNATE_GPO = "browser.policies.alternateGPO";
+
+// For testing, we may want to set PREF_ALTERNATE_PATH to point to a file
+// relative to the test root directory. In order to enable this, the string
+// below may be placed at the beginning of that preference value and it will
+// be replaced with the path to the test root directory.
+const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
+const PREF_TEST_ROOT = "mochitest.testRoot";
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+// To force disallowing enterprise-only policies during tests
+const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
+
+// To allow for cleaning up old policies
+const PREF_POLICIES_APPLIED = "browser.policies.applied";
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ return new ConsoleAPI({
+ prefix: "Enterprise Policies",
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+const isXpcshell = Services.env.exists("XPCSHELL_TEST_PROFILE_DIR");
+
+// We're only testing for empty objects, not
+// empty strings or empty arrays.
+function isEmptyObject(obj) {
+ if (typeof obj != "object" || Array.isArray(obj)) {
+ return false;
+ }
+ for (let key of Object.keys(obj)) {
+ if (!isEmptyObject(obj[key])) {
+ return false;
+ }
+ }
+ return true;
+}
+
+export function EnterprisePoliciesManager() {
+ Services.obs.addObserver(this, "profile-after-change", true);
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", true);
+ Services.obs.addObserver(this, "EnterprisePolicies:Restart", true);
+}
+
+EnterprisePoliciesManager.prototype = {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ "nsIEnterprisePolicies",
+ ]),
+
+ _initialize() {
+ if (Services.prefs.getBoolPref(PREF_POLICIES_APPLIED, false)) {
+ if ("_cleanup" in lazy.Policies) {
+ let policyImpl = lazy.Policies._cleanup;
+
+ for (let timing of Object.keys(this._callbacks)) {
+ let policyCallback = policyImpl[timing];
+ if (policyCallback) {
+ this._schedulePolicyCallback(
+ timing,
+ policyCallback.bind(
+ policyImpl,
+ this /* the EnterprisePoliciesManager */
+ )
+ );
+ }
+ }
+ }
+ Services.prefs.clearUserPref(PREF_POLICIES_APPLIED);
+ }
+
+ let provider = this._chooseProvider();
+
+ if (provider.failed) {
+ this.status = Ci.nsIEnterprisePolicies.FAILED;
+ this._reportEnterpriseTelemetry();
+ return;
+ }
+
+ if (!provider.hasPolicies) {
+ this.status = Ci.nsIEnterprisePolicies.INACTIVE;
+ this._reportEnterpriseTelemetry();
+ return;
+ }
+
+ this.status = Ci.nsIEnterprisePolicies.ACTIVE;
+ this._parsedPolicies = {};
+ this._reportEnterpriseTelemetry(provider.policies);
+ this._activatePolicies(provider.policies);
+
+ Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true);
+ },
+
+ _reportEnterpriseTelemetry(policies = {}) {
+ let excludedDistributionIDs = [
+ "mozilla-mac-eol-esr115",
+ "mozilla-win-eol-esr115",
+ ];
+ let distroId = Services.prefs
+ .getDefaultBranch(null)
+ .getCharPref("distribution.id", "");
+
+ let policiesLength = Object.keys(policies).length;
+
+ Services.telemetry.scalarSet("policies.count", policiesLength);
+
+ let isEnterprise =
+ // As we migrate folks to ESR for other reasons (deprecating an OS),
+ // we need to add checks here for distribution IDs.
+ (AppConstants.IS_ESR && !excludedDistributionIDs.includes(distroId)) ||
+ // If there are multiple policies then its enterprise.
+ policiesLength > 1 ||
+ // If ImportEnterpriseRoots isn't the only policy then it's enterprise.
+ (policiesLength && !policies.Certificates?.ImportEnterpriseRoots);
+
+ Services.telemetry.scalarSet("policies.is_enterprise", isEnterprise);
+ },
+
+ _chooseProvider() {
+ let platformProvider = null;
+ if (AppConstants.platform == "win" && AppConstants.MOZ_SYSTEM_POLICIES) {
+ platformProvider = new WindowsGPOPoliciesProvider();
+ } else if (
+ AppConstants.platform == "macosx" &&
+ AppConstants.MOZ_SYSTEM_POLICIES
+ ) {
+ platformProvider = new macOSPoliciesProvider();
+ }
+ let jsonProvider = new JSONPoliciesProvider();
+ if (platformProvider && platformProvider.hasPolicies) {
+ if (jsonProvider.hasPolicies) {
+ return new CombinedProvider(platformProvider, jsonProvider);
+ }
+ return platformProvider;
+ }
+ return jsonProvider;
+ },
+
+ _activatePolicies(unparsedPolicies) {
+ let { schema } = ChromeUtils.importESModule(
+ "resource:///modules/policies/schema.sys.mjs"
+ );
+
+ for (let policyName of Object.keys(unparsedPolicies)) {
+ let policySchema = schema.properties[policyName];
+ let policyParameters = unparsedPolicies[policyName];
+
+ if (!policySchema) {
+ lazy.log.error(`Unknown policy: ${policyName}`);
+ continue;
+ }
+
+ if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
+ lazy.log.error(`Policy ${policyName} is only allowed on ESR`);
+ continue;
+ }
+
+ let { valid: parametersAreValid, parsedValue: parsedParameters } =
+ lazy.JsonSchemaValidator.validate(policyParameters, policySchema, {
+ allowAdditionalProperties: true,
+ });
+
+ if (!parametersAreValid) {
+ lazy.log.error(`Invalid parameters specified for ${policyName}.`);
+ continue;
+ }
+
+ let policyImpl = lazy.Policies[policyName];
+
+ if (policyImpl.validate && !policyImpl.validate(parsedParameters)) {
+ lazy.log.error(
+ `Parameters for ${policyName} did not validate successfully.`
+ );
+ continue;
+ }
+
+ this._parsedPolicies[policyName] = parsedParameters;
+
+ for (let timing of Object.keys(this._callbacks)) {
+ let policyCallback = policyImpl[timing];
+ if (policyCallback) {
+ this._schedulePolicyCallback(
+ timing,
+ policyCallback.bind(
+ policyImpl,
+ this /* the EnterprisePoliciesManager */,
+ parsedParameters
+ )
+ );
+ }
+ }
+ }
+ },
+
+ _callbacks: {
+ // The earliest that a policy callback can run. This will
+ // happen right after the Policy Engine itself has started,
+ // and before the Add-ons Manager has started.
+ onBeforeAddons: [],
+
+ // This happens after all the initialization related to
+ // the profile has finished (prefs, places database, etc.).
+ onProfileAfterChange: [],
+
+ // Just before the first browser window gets created.
+ onBeforeUIStartup: [],
+
+ // Called after all windows from the last session have been
+ // restored (or the default window and homepage tab, if the
+ // session is not being restored).
+ // The content of the tabs themselves have not necessarily
+ // finished loading.
+ onAllWindowsRestored: [],
+ },
+
+ _schedulePolicyCallback(timing, callback) {
+ this._callbacks[timing].push(callback);
+ },
+
+ _runPoliciesCallbacks(timing) {
+ let callbacks = this._callbacks[timing];
+ while (callbacks.length) {
+ let callback = callbacks.shift();
+ try {
+ callback();
+ } catch (ex) {
+ lazy.log.error("Error running ", callback, `for ${timing}:`, ex);
+ }
+ }
+ },
+
+ async _restart() {
+ DisallowedFeatures = {};
+
+ Services.ppmm.sharedData.delete("EnterprisePolicies:Status");
+ Services.ppmm.sharedData.delete("EnterprisePolicies:DisallowedFeatures");
+
+ this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED;
+ this._parsedPolicies = undefined;
+ for (let timing of Object.keys(this._callbacks)) {
+ this._callbacks[timing] = [];
+ }
+
+ // Simulate the startup process. This step-by-step is a bit ugly but it
+ // tries to emulate the same behavior as of a normal startup.
+ let notifyTopicOnIdle = topic =>
+ new Promise(resolve => {
+ ChromeUtils.idleDispatch(() => {
+ this.observe(null, topic, "");
+ resolve();
+ });
+ });
+ await notifyTopicOnIdle("policies-startup");
+ await notifyTopicOnIdle("profile-after-change");
+ await notifyTopicOnIdle("final-ui-startup");
+ await notifyTopicOnIdle("sessionstore-windows-restored");
+ },
+
+ // nsIObserver implementation
+ observe: function BG_observe(subject, topic, data) {
+ switch (topic) {
+ case "policies-startup":
+ // Before the first set of policy callbacks runs, we must
+ // initialize the service.
+ this._initialize();
+
+ this._runPoliciesCallbacks("onBeforeAddons");
+ break;
+
+ case "profile-after-change":
+ this._runPoliciesCallbacks("onProfileAfterChange");
+ break;
+
+ case "final-ui-startup":
+ this._runPoliciesCallbacks("onBeforeUIStartup");
+ break;
+
+ case "sessionstore-windows-restored":
+ this._runPoliciesCallbacks("onAllWindowsRestored");
+
+ // After the last set of policy callbacks ran, notify the test observer.
+ Services.obs.notifyObservers(
+ null,
+ "EnterprisePolicies:AllPoliciesApplied"
+ );
+ break;
+
+ case "EnterprisePolicies:Restart":
+ this._restart().then(null, console.error);
+ break;
+ }
+ },
+
+ disallowFeature(feature, neededOnContentProcess = false) {
+ DisallowedFeatures[feature] = neededOnContentProcess;
+
+ // NOTE: For optimization purposes, only features marked as needed
+ // on content process will be passed onto the child processes.
+ if (neededOnContentProcess) {
+ Services.ppmm.sharedData.set(
+ "EnterprisePolicies:DisallowedFeatures",
+ new Set(
+ Object.keys(DisallowedFeatures).filter(key => DisallowedFeatures[key])
+ )
+ );
+ }
+ },
+
+ // ------------------------------
+ // public nsIEnterprisePolicies members
+ // ------------------------------
+
+ _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,
+
+ set status(val) {
+ this._status = val;
+ if (val != Ci.nsIEnterprisePolicies.INACTIVE) {
+ Services.ppmm.sharedData.set("EnterprisePolicies:Status", val);
+ }
+ },
+
+ get status() {
+ return this._status;
+ },
+
+ isAllowed: function BG_sanitize(feature) {
+ return !(feature in DisallowedFeatures);
+ },
+
+ getActivePolicies() {
+ return this._parsedPolicies;
+ },
+
+ setSupportMenu(supportMenu) {
+ SupportMenu = supportMenu;
+ },
+
+ getSupportMenu() {
+ return SupportMenu;
+ },
+
+ setExtensionPolicies(extensionPolicies) {
+ ExtensionPolicies = extensionPolicies;
+ },
+
+ getExtensionPolicy(extensionID) {
+ if (ExtensionPolicies && extensionID in ExtensionPolicies) {
+ return ExtensionPolicies[extensionID];
+ }
+ return null;
+ },
+
+ setExtensionSettings(extensionSettings) {
+ ExtensionSettings = extensionSettings;
+ if (
+ "*" in extensionSettings &&
+ "install_sources" in extensionSettings["*"]
+ ) {
+ InstallSources = new MatchPatternSet(
+ extensionSettings["*"].install_sources
+ );
+ }
+ },
+
+ getExtensionSettings(extensionID) {
+ let settings = null;
+ if (ExtensionSettings) {
+ if (extensionID in ExtensionSettings) {
+ settings = ExtensionSettings[extensionID];
+ } else if ("*" in ExtensionSettings) {
+ settings = ExtensionSettings["*"];
+ }
+ }
+ return settings;
+ },
+
+ mayInstallAddon(addon) {
+ // See https://dev.chromium.org/administrators/policy-list-3/extension-settings-full
+ if (!ExtensionSettings) {
+ return true;
+ }
+ if (addon.id in ExtensionSettings) {
+ if ("installation_mode" in ExtensionSettings[addon.id]) {
+ switch (ExtensionSettings[addon.id].installation_mode) {
+ case "blocked":
+ return false;
+ default:
+ return true;
+ }
+ }
+ }
+ if ("*" in ExtensionSettings) {
+ if (
+ ExtensionSettings["*"].installation_mode &&
+ ExtensionSettings["*"].installation_mode == "blocked"
+ ) {
+ return false;
+ }
+ if ("allowed_types" in ExtensionSettings["*"]) {
+ return ExtensionSettings["*"].allowed_types.includes(addon.type);
+ }
+ }
+ return true;
+ },
+
+ allowedInstallSource(uri) {
+ return InstallSources ? InstallSources.matches(uri) : true;
+ },
+
+ isExemptExecutableExtension(url, extension) {
+ let urlObject;
+ try {
+ urlObject = new URL(url);
+ } catch (e) {
+ return false;
+ }
+ let { hostname } = urlObject;
+ let exemptArray =
+ this.getActivePolicies()
+ ?.ExemptDomainFileTypePairsFromFileTypeDownloadWarnings;
+ if (!hostname || !extension || !exemptArray) {
+ return false;
+ }
+ extension = extension.toLowerCase();
+ let domains = exemptArray
+ .filter(item => item.file_extension.toLowerCase() == extension)
+ .map(item => item.domains)
+ .flat();
+ for (let domain of domains) {
+ if (Services.eTLD.hasRootDomain(hostname, domain)) {
+ return true;
+ }
+ }
+ return false;
+ },
+};
+
+let DisallowedFeatures = {};
+let SupportMenu = null;
+let ExtensionPolicies = null;
+let ExtensionSettings = null;
+let InstallSources = null;
+
+/**
+ * areEnterpriseOnlyPoliciesAllowed
+ *
+ * Checks whether the policies marked as enterprise_only in the
+ * schema are allowed to run on this browser.
+ *
+ * This is meant to only allow policies to run on ESR, but in practice
+ * we allow it to run on channels different than release, to allow
+ * these policies to be tested on pre-release channels.
+ *
+ * @returns {Bool} Whether the policy can run.
+ */
+function areEnterpriseOnlyPoliciesAllowed() {
+ if (Cu.isInAutomation || isXpcshell) {
+ if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
+ // This is used as an override to test the "enterprise_only"
+ // functionality itself on tests.
+ return false;
+ }
+ return true;
+ }
+
+ return (
+ AppConstants.IS_ESR ||
+ AppConstants.MOZ_DEV_EDITION ||
+ AppConstants.NIGHTLY_BUILD
+ );
+}
+
+/*
+ * JSON PROVIDER OF POLICIES
+ *
+ * This is a platform-agnostic provider which looks for
+ * policies specified through a policies.json file stored
+ * in the installation's distribution folder.
+ */
+
+class JSONPoliciesProvider {
+ constructor() {
+ this._policies = null;
+ this._readData();
+ }
+
+ get hasPolicies() {
+ return this._policies !== null && !isEmptyObject(this._policies);
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ return this._failed;
+ }
+
+ _getConfigurationFile() {
+ let configFile = null;
+
+ if (AppConstants.platform == "linux" && AppConstants.MOZ_SYSTEM_POLICIES) {
+ let systemConfigFile = Services.dirsvc.get("SysConfD", Ci.nsIFile);
+ systemConfigFile.append("policies");
+ systemConfigFile.append(POLICIES_FILENAME);
+ if (systemConfigFile.exists()) {
+ return systemConfigFile;
+ }
+ }
+
+ try {
+ let perUserPath = Services.prefs.getBoolPref(PREF_PER_USER_DIR, false);
+ if (perUserPath) {
+ configFile = Services.dirsvc.get("XREUserRunTimeDir", Ci.nsIFile);
+ } else {
+ configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
+ }
+ configFile.append(POLICIES_FILENAME);
+ } catch (ex) {
+ // Getting the correct directory will fail in xpcshell tests. This should
+ // be handled the same way as if the configFile simply does not exist.
+ }
+
+ let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH, "");
+
+ // Check if we are in automation *before* we use the synchronous
+ // nsIFile.exists() function or allow the config file to be overriden
+ // An alternate policy path can also be used in Nightly builds (for
+ // testing purposes), but the Background Update Agent will be unable to
+ // detect the alternate policy file so the DisableAppUpdate policy may not
+ // work as expected.
+ if (
+ alternatePath &&
+ (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) &&
+ (!configFile || !configFile.exists())
+ ) {
+ if (alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
+ // Intentionally not using a default value on this pref lookup. If no
+ // test root is set, we are not currently testing and this function
+ // should throw rather than returning something.
+ let testRoot = Services.prefs.getStringPref(PREF_TEST_ROOT);
+ let relativePath = alternatePath.substring(
+ MAGIC_TEST_ROOT_PREFIX.length
+ );
+ if (AppConstants.platform == "win") {
+ relativePath = relativePath.replace(/\//g, "\\");
+ }
+ alternatePath = testRoot + relativePath;
+ }
+
+ configFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ configFile.initWithPath(alternatePath);
+ }
+
+ return configFile;
+ }
+
+ _readData() {
+ let configFile = this._getConfigurationFile();
+ if (!configFile) {
+ // Do nothing, _policies will remain null
+ return;
+ }
+ try {
+ let data = Cu.readUTF8File(configFile);
+ if (data) {
+ lazy.log.debug(`policies.json path = ${configFile.path}`);
+ lazy.log.debug(`policies.json content = ${data}`);
+ this._policies = JSON.parse(data).policies;
+
+ if (!this._policies) {
+ lazy.log.error("Policies file doesn't contain a 'policies' object");
+ this._failed = true;
+ }
+ }
+ } catch (ex) {
+ if (
+ ex instanceof Components.Exception &&
+ ex.result == Cr.NS_ERROR_FILE_NOT_FOUND
+ ) {
+ // Do nothing, _policies will remain null
+ } else if (ex instanceof SyntaxError) {
+ lazy.log.error(`Error parsing JSON file: ${ex}`);
+ this._failed = true;
+ } else {
+ lazy.log.error(`Error reading JSON file: ${ex}`);
+ this._failed = true;
+ }
+ }
+ }
+}
+
+class WindowsGPOPoliciesProvider {
+ constructor() {
+ this._policies = null;
+
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+
+ // Machine policies override user policies, so we read
+ // user policies first and then replace them if necessary.
+ this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
+ // We don't access machine policies in testing
+ if (!Cu.isInAutomation && !isXpcshell) {
+ this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
+ }
+ }
+
+ get hasPolicies() {
+ return this._policies !== null && !isEmptyObject(this._policies);
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ return this._failed;
+ }
+
+ _readData(wrk, root) {
+ try {
+ let regLocation = "SOFTWARE\\Policies";
+ if (Cu.isInAutomation || isXpcshell) {
+ try {
+ regLocation = Services.prefs.getStringPref(PREF_ALTERNATE_GPO);
+ } catch (e) {}
+ }
+ wrk.open(root, regLocation, wrk.ACCESS_READ);
+ if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
+ lazy.log.debug(
+ `root = ${
+ root == wrk.ROOT_KEY_CURRENT_USER
+ ? "HKEY_CURRENT_USER"
+ : "HKEY_LOCAL_MACHINE"
+ }`
+ );
+ this._policies = lazy.WindowsGPOParser.readPolicies(
+ wrk,
+ this._policies
+ );
+ }
+ wrk.close();
+ } catch (e) {
+ lazy.log.error("Unable to access registry - ", e);
+ }
+ }
+}
+
+class macOSPoliciesProvider {
+ constructor() {
+ this._policies = null;
+ let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance(
+ Ci.nsIMacPreferencesReader
+ );
+ if (!prefReader.policiesEnabled()) {
+ return;
+ }
+ this._policies = lazy.macOSPoliciesParser.readPolicies(prefReader);
+ }
+
+ get hasPolicies() {
+ return this._policies !== null && Object.keys(this._policies).length;
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ return this._failed;
+ }
+}
+
+class CombinedProvider {
+ constructor(primaryProvider, secondaryProvider) {
+ // Combine policies with primaryProvider taking precedence.
+ // We only do this for top level policies.
+ this._policies = primaryProvider._policies;
+ for (let policyName of Object.keys(secondaryProvider.policies)) {
+ if (!(policyName in this._policies)) {
+ this._policies[policyName] = secondaryProvider.policies[policyName];
+ }
+ }
+ }
+
+ get hasPolicies() {
+ // Combined provider always has policies.
+ return true;
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ // Combined provider never fails.
+ return false;
+ }
+}
diff --git a/toolkit/components/enterprisepolicies/WindowsGPOParser.sys.mjs b/toolkit/components/enterprisepolicies/WindowsGPOParser.sys.mjs
new file mode 100644
index 0000000000..be2c035e7f
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/WindowsGPOParser.sys.mjs
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ return new ConsoleAPI({
+ prefix: "GPOParser.jsm",
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+export var WindowsGPOParser = {
+ readPolicies(wrk, policies) {
+ let childWrk = wrk.openChild(
+ "Mozilla\\" + Services.appinfo.name,
+ wrk.ACCESS_READ
+ );
+ if (!policies) {
+ policies = {};
+ }
+ try {
+ policies = registryToObject(childWrk, policies);
+ } catch (e) {
+ lazy.log.error(e);
+ } finally {
+ childWrk.close();
+ }
+ // Need an extra check here so we don't
+ // JSON.stringify if we aren't in debug mode
+ if (lazy.log._maxLogLevel == "debug") {
+ lazy.log.debug(JSON.stringify(policies, null, 2));
+ }
+ return policies;
+ },
+};
+
+function registryToObject(wrk, policies) {
+ if (!policies) {
+ policies = {};
+ }
+ if (wrk.valueCount > 0) {
+ if (wrk.getValueName(0) == "1") {
+ // If the first item is 1, just assume it is an array
+ let array = [];
+ for (let i = 0; i < wrk.valueCount; i++) {
+ array.push(readRegistryValue(wrk, wrk.getValueName(i)));
+ }
+ // If it's an array, it shouldn't have any children
+ return array;
+ }
+ for (let i = 0; i < wrk.valueCount; i++) {
+ let name = wrk.getValueName(i);
+ let value = readRegistryValue(wrk, name);
+ if (value != undefined) {
+ policies[name] = value;
+ }
+ }
+ }
+ if (wrk.childCount > 0) {
+ if (wrk.getChildName(0) == "1") {
+ // If the first item is 1, it's an array of objects
+ let array = [];
+ for (let i = 0; i < wrk.childCount; i++) {
+ let name = wrk.getChildName(i);
+ let childWrk = wrk.openChild(name, wrk.ACCESS_READ);
+ array.push(registryToObject(childWrk));
+ childWrk.close();
+ }
+ // If it's an array, it shouldn't have any children
+ return array;
+ }
+ for (let i = 0; i < wrk.childCount; i++) {
+ let name = wrk.getChildName(i);
+ let childWrk = wrk.openChild(name, wrk.ACCESS_READ);
+ policies[name] = registryToObject(childWrk);
+ childWrk.close();
+ }
+ }
+ return policies;
+}
+
+function readRegistryValue(wrk, value) {
+ switch (wrk.getValueType(value)) {
+ case 7: // REG_MULTI_SZ
+ // While we support JSON in REG_SZ and REG_MULTI_SZ, if it's REG_MULTI_SZ,
+ // we know it must be JSON. So we go ahead and JSON.parse it here so it goes
+ // through the schema validator.
+ try {
+ return JSON.parse(wrk.readStringValue(value).replace(/\0/g, "\n"));
+ } catch (e) {
+ lazy.log.error(`Unable to parse JSON for ${value}`);
+ return undefined;
+ }
+ case 2: // REG_EXPAND_SZ
+ case wrk.TYPE_STRING:
+ return wrk.readStringValue(value);
+ case wrk.TYPE_BINARY:
+ return wrk.readBinaryValue(value);
+ case wrk.TYPE_INT:
+ return wrk.readIntValue(value);
+ case wrk.TYPE_INT64:
+ return wrk.readInt64Value(value);
+ }
+ // unknown type
+ return null;
+}
diff --git a/toolkit/components/enterprisepolicies/components.conf b/toolkit/components/enterprisepolicies/components.conf
new file mode 100644
index 0000000000..fc5ce14d51
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/components.conf
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'js_name': 'policies',
+ 'cid': '{49e8d8ef-a713-492a-a3d2-5c9dad4ce2e5}',
+ 'contract_ids': ['@mozilla.org/enterprisepolicies;1'],
+ 'interfaces': ['nsIEnterprisePolicies'],
+ 'esModule': 'resource://gre/modules/EnterprisePolicies.sys.mjs',
+ 'constructor': 'EnterprisePolicies',
+ },
+]
diff --git a/toolkit/components/enterprisepolicies/macOSPoliciesParser.sys.mjs b/toolkit/components/enterprisepolicies/macOSPoliciesParser.sys.mjs
new file mode 100644
index 0000000000..9786d3b374
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/macOSPoliciesParser.sys.mjs
@@ -0,0 +1,158 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.importESModule(
+ "resource://gre/modules/Console.sys.mjs"
+ );
+ return new ConsoleAPI({
+ prefix: "macOSPoliciesParser.jsm",
+ // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
+ // messages during development. See LOG_LEVELS in Console.sys.mjs for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+export var macOSPoliciesParser = {
+ readPolicies(reader) {
+ let nativePolicies = reader.readPreferences();
+ if (!nativePolicies) {
+ return null;
+ }
+
+ nativePolicies = this.unflatten(nativePolicies);
+ nativePolicies = this.removeUnknownPolicies(nativePolicies);
+
+ // Need an extra check here so we don't
+ // JSON.stringify if we aren't in debug mode
+ if (lazy.log.maxLogLevel == "debug") {
+ lazy.log.debug(JSON.stringify(nativePolicies, null, 2));
+ }
+
+ return nativePolicies;
+ },
+
+ removeUnknownPolicies(policies) {
+ let { schema } = ChromeUtils.importESModule(
+ "resource:///modules/policies/schema.sys.mjs"
+ );
+
+ for (let policyName of Object.keys(policies)) {
+ if (!schema.properties.hasOwnProperty(policyName)) {
+ lazy.log.debug(`Removing unknown policy: ${policyName}`);
+ delete policies[policyName];
+ }
+ }
+
+ return policies;
+ },
+
+ unflatten(input, delimiter = "__") {
+ let ret = {};
+
+ for (let key of Object.keys(input)) {
+ if (!key.includes(delimiter)) {
+ // Short-circuit for policies that are not specified in
+ // the flat format.
+ ret[key] = input[key];
+ continue;
+ }
+
+ lazy.log.debug(`Unflattening policy key "${key}".`);
+
+ let subkeys = key.split(delimiter);
+
+ // `obj`: is the intermediate step into the unflattened
+ // return object. For example, for an input:
+ //
+ // Foo__Bar__Baz: 5,
+ //
+ // when the subkey being iterated is Bar, then `obj` will be
+ // the Bar object being constructed, as represented below:
+ //
+ // ret = {
+ // Foo = {
+ // Bar = { <---- obj
+ // Baz: 5,
+ // }
+ // }
+ // }
+ let obj = ret;
+
+ // Iterate until the second to last subkey, as the last one
+ // needs special handling afterwards.
+ for (let i = 0; i < subkeys.length - 1; i++) {
+ let subkey = subkeys[i];
+
+ if (!isValidSubkey(subkey)) {
+ lazy.log.error(
+ `Error in key ${key}: can't use indexes bigger than 50.`
+ );
+ continue;
+ }
+
+ if (!obj[subkey]) {
+ // if this subkey hasn't been seen yet, create the object
+ // for it, which could be an array if the next subkey is
+ // a number.
+ //
+ // For example, in the following examples:
+ // A)
+ // Foo__Bar__0
+ // Foo__Bar__1
+ //
+ // B)
+ // Foo__Bar__Baz
+ // Foo__Bar__Qux
+ //
+ // If the subkey being analysed right now is Bar, then in example A
+ // we'll create an array to accomodate the numeric entries.
+ // Otherwise, if it's example B, we'll create an object to host all
+ // the named keys.
+ if (Number.isInteger(Number(subkeys[i + 1]))) {
+ obj[subkey] = [];
+ } else {
+ obj[subkey] = {};
+ }
+ }
+
+ obj = obj[subkey];
+ }
+
+ let lastSubkey = subkeys[subkeys.length - 1];
+ if (!isValidSubkey(lastSubkey)) {
+ lazy.log.error(
+ `Error in key ${key}: can't use indexes bigger than 50.`
+ );
+ continue;
+ }
+
+ // In the last subkey, we assign it the value by accessing the input
+ // object again with the full key. For example, in the case:
+ //
+ // input = {"Foo__Bar__Baz": 5}
+ //
+ // what we're doing in practice is:
+ //
+ // ret["Foo"]["Bar"]["Baz"] = input["Foo__Bar__Baz"];
+ // \_______ _______/ |
+ // v |
+ // obj last subkey
+
+ obj[lastSubkey] = input[key];
+ }
+
+ return ret;
+ },
+};
+
+function isValidSubkey(subkey) {
+ let valueAsNumber = Number(subkey);
+ return Number.isNaN(valueAsNumber) || valueAsNumber <= 50;
+}
diff --git a/toolkit/components/enterprisepolicies/moz.build b/toolkit/components/enterprisepolicies/moz.build
new file mode 100644
index 0000000000..4fcb377c80
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/moz.build
@@ -0,0 +1,37 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Firefox", "Enterprise Policies")
+
+XPIDL_SOURCES += [
+ "nsIEnterprisePolicies.idl",
+]
+
+XPIDL_MODULE = "enterprisepolicies"
+
+TEST_DIRS += ["tests"]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] != "android":
+ EXTRA_JS_MODULES += [
+ "EnterprisePolicies.sys.mjs",
+ "EnterprisePoliciesContent.sys.mjs",
+ "EnterprisePoliciesParent.sys.mjs",
+ ]
+
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ EXTRA_JS_MODULES.policies += [
+ "WindowsGPOParser.sys.mjs",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ EXTRA_JS_MODULES.policies += [
+ "macOSPoliciesParser.sys.mjs",
+ ]
diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl
new file mode 100644
index 0000000000..03c51af49f
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIURI.idl"
+
+[scriptable, uuid(6a568972-cc91-4bf5-963e-3768f3319b8a)]
+interface nsIEnterprisePolicies : nsISupports
+{
+ const short UNINITIALIZED = -1;
+ const short INACTIVE = 0;
+ const short ACTIVE = 1;
+ const short FAILED = 2;
+
+ readonly attribute short status;
+
+ bool isAllowed(in ACString feature);
+
+ /**
+ * Get the active policies that have been successfully parsed.
+ *
+ * @returns A JS object that contains the policies names and
+ * their corresponding parameters.
+ */
+ jsval getActivePolicies();
+
+ /**
+ * Get the contents of the support menu (if applicable)
+ *
+ * @returns A JS object that contains the url and label or null.
+ */
+ jsval getSupportMenu();
+
+ /**
+ * Get the policy for a given extensionID (if available)
+ *
+ * @returns A JS object that contains the storage or null if unavailable.
+ */
+ jsval getExtensionPolicy(in ACString extensionID);
+
+ /**
+ * Retrieves the ExtensionSettings policy for the given extensionID.
+ *
+ * If there is no policy for the extension, it returns the global policy.
+ *
+ * If there is no global policy, it returns null.
+ *
+ * @returns A JS object that settings or null if unavailable.
+ */
+ jsval getExtensionSettings(in ACString extensionID);
+
+ /**
+ * Uses the allowlist, blocklist and settings to determine if an addon
+ * may be installed.
+ *
+ * @returns A boolean - true of the addon may be installed.
+ */
+ bool mayInstallAddon(in jsval addon);
+
+ /**
+ * Uses install_sources to determine if an addon can be installed
+ * from the given URI.
+ *
+ * @returns A boolean - true of the addon may be installed.
+ */
+ bool allowedInstallSource(in nsIURI uri);
+ /**
+ * Uses ExemptDomainFileTypePairsFromFileTypeDownloadWarnings to determine
+ * if a given file extension is exempted from executable behavior and
+ * warnings based on the URL.
+ *
+ * @returns A boolean - true if the extension should be exempt.
+ */
+ bool isExemptExecutableExtension(in ACString url, in ACString extension);
+};
diff --git a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs
new file mode 100644
index 0000000000..b877c6f738
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Preferences } from "resource://gre/modules/Preferences.sys.mjs";
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs",
+ modifySchemaForTests: "resource:///modules/policies/schema.sys.mjs",
+});
+
+export var EnterprisePolicyTesting = {
+ // |json| must be an object representing the desired policy configuration, OR a
+ // path to the JSON file containing the policy configuration.
+ setupPolicyEngineWithJson: async function setupPolicyEngineWithJson(
+ json,
+ customSchema
+ ) {
+ let filePath;
+ if (typeof json == "object") {
+ filePath = lazy.FileTestUtils.getTempFile("policies.json").path;
+
+ // This file gets automatically deleted by FileTestUtils
+ // at the end of the test run.
+ await IOUtils.writeJSON(filePath, json);
+ } else {
+ filePath = json;
+ }
+
+ Services.prefs.setStringPref("browser.policies.alternatePath", filePath);
+
+ let promise = new Promise(resolve => {
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(
+ observer,
+ "EnterprisePolicies:AllPoliciesApplied"
+ );
+ resolve();
+ }, "EnterprisePolicies:AllPoliciesApplied");
+ });
+
+ // Clear any previously used custom schema or assign a new one
+ lazy.modifySchemaForTests(customSchema || null);
+
+ Services.obs.notifyObservers(null, "EnterprisePolicies:Restart");
+ return promise;
+ },
+
+ checkPolicyPref(prefName, expectedValue, expectedLockedness) {
+ if (expectedLockedness !== undefined) {
+ Assert.equal(
+ Preferences.locked(prefName),
+ expectedLockedness,
+ `Pref ${prefName} is correctly locked/unlocked`
+ );
+ }
+
+ Assert.equal(
+ Preferences.get(prefName),
+ expectedValue,
+ `Pref ${prefName} has the correct value`
+ );
+ },
+
+ resetRunOnceState: function resetRunOnceState() {
+ const runOnceBaseKeys = [
+ "browser.policies.runonce.",
+ "browser.policies.runOncePerModification.",
+ ];
+ for (let base of runOnceBaseKeys) {
+ for (let key of Services.prefs.getChildList(base)) {
+ if (Services.prefs.prefHasUserValue(key)) {
+ Services.prefs.clearUserPref(key);
+ }
+ }
+ }
+ },
+};
+
+/**
+ * This helper will track prefs that have been changed
+ * by the policy engine through the setAndLockPref and
+ * setDefaultPref APIs (from Policies.jsm) and make sure
+ * that they are restored to their original values when
+ * the test ends or another test case restarts the engine.
+ */
+export var PoliciesPrefTracker = {
+ _originalFunc: null,
+ _originalValues: new Map(),
+
+ start() {
+ let { PoliciesUtils } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+ this._originalFunc = PoliciesUtils.setDefaultPref;
+ PoliciesUtils.setDefaultPref = this.hoistedSetDefaultPref.bind(this);
+ },
+
+ stop() {
+ this.restoreDefaultValues();
+
+ let { PoliciesUtils } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+ PoliciesUtils.setDefaultPref = this._originalFunc;
+ this._originalFunc = null;
+ },
+
+ hoistedSetDefaultPref(prefName, prefValue, locked = false) {
+ // If this pref is seen multiple times, the very first
+ // value seen is the one that is actually the default.
+ if (!this._originalValues.has(prefName)) {
+ let defaults = new Preferences({ defaultBranch: true });
+ let stored = {};
+
+ if (defaults.has(prefName)) {
+ stored.originalDefaultValue = defaults.get(prefName);
+ } else {
+ stored.originalDefaultValue = undefined;
+ }
+
+ if (
+ Preferences.isSet(prefName) &&
+ Preferences.get(prefName) == prefValue
+ ) {
+ // If a user value exists, and we're changing the default
+ // value to be th same as the user value, that will cause
+ // the user value to be dropped. In that case, let's also
+ // store it to ensure that we restore everything correctly.
+ stored.originalUserValue = Preferences.get(prefName);
+ }
+
+ this._originalValues.set(prefName, stored);
+ }
+
+ // Now that we've stored the original values, call the
+ // original setDefaultPref function.
+ this._originalFunc(prefName, prefValue, locked);
+ },
+
+ restoreDefaultValues() {
+ let defaults = new Preferences({ defaultBranch: true });
+
+ for (let [prefName, stored] of this._originalValues) {
+ // If a pref was used through setDefaultPref instead
+ // of setAndLockPref, it wasn't locked, but calling
+ // unlockPref is harmless
+ Preferences.unlock(prefName);
+
+ if (stored.originalDefaultValue !== undefined) {
+ defaults.set(prefName, stored.originalDefaultValue);
+ } else {
+ Services.prefs.getDefaultBranch("").deleteBranch(prefName);
+ }
+
+ if (stored.originalUserValue !== undefined) {
+ Preferences.set(prefName, stored.originalUserValue);
+ }
+ }
+
+ this._originalValues.clear();
+ },
+};
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser.toml b/toolkit/components/enterprisepolicies/tests/browser/browser.toml
new file mode 100644
index 0000000000..1b76676779
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+head = "head.js"
+support-files = ["config_broken_json.json"]
+
+["browser_policies_basic_tests.js"]
+
+["browser_policies_broken_json.js"]
+
+["browser_policies_enterprise_only.js"]
+
+["browser_policies_gpo.js"]
+skip-if = ["os != 'win'"]
+
+["browser_policies_mistyped_json.js"]
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js
new file mode 100644
index 0000000000..8efcdee316
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_simple_policies() {
+ let { Policies } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+
+ let policy0Ran = false,
+ policy1Ran = false,
+ policy2Ran = false,
+ policy3Ran = false;
+
+ // Implement functions to handle the four simple policies that will be added
+ // to the schema.
+ Policies.simple_policy0 = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was passed in config file");
+ policy0Ran = true;
+ },
+ };
+
+ Policies.simple_policy1 = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was passed in config file");
+ manager.disallowFeature("feature1", /* needed in content process */ true);
+ policy1Ran = true;
+ },
+ };
+
+ Policies.simple_policy2 = {
+ onBeforeUIStartup(manager, param) {
+ is(param, true, "Param matches what was passed in config file");
+ manager.disallowFeature(
+ "feature2",
+ /* needed in content process */ false
+ );
+ policy2Ran = true;
+ },
+ };
+
+ Policies.simple_policy3 = {
+ onAllWindowsRestored(manager, param) {
+ is(param, false, "Param matches what was passed in config file");
+ policy3Ran = true;
+ },
+ };
+
+ await setupPolicyEngineWithJson(
+ // policies.json
+ {
+ policies: {
+ simple_policy0: true,
+ simple_policy1: true,
+ simple_policy2: true,
+ simple_policy3: false,
+ },
+ },
+
+ // custom schema
+ {
+ properties: {
+ simple_policy0: {
+ type: "boolean",
+ },
+
+ simple_policy1: {
+ type: "boolean",
+ },
+
+ simple_policy2: {
+ type: "boolean",
+ },
+
+ simple_policy3: {
+ type: "boolean",
+ },
+ },
+ }
+ );
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Engine is active"
+ );
+ is(
+ Services.policies.isAllowed("feature1"),
+ false,
+ "Dummy feature was disallowed"
+ );
+ is(
+ Services.policies.isAllowed("feature2"),
+ false,
+ "Dummy feature was disallowed"
+ );
+
+ ok(policy0Ran, "Policy 0 ran correctly through BeforeAddons");
+ ok(policy1Ran, "Policy 1 ran correctly through onProfileAfterChange");
+ ok(policy2Ran, "Policy 2 ran correctly through onBeforeUIStartup");
+ ok(policy3Ran, "Policy 3 ran correctly through onAllWindowsRestored");
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], async function () {
+ if (Services.appinfo.processType == Services.appinfo.PROCESS_TYPE_CONTENT) {
+ is(
+ Services.policies.isAllowed("feature1"),
+ false,
+ "Correctly disallowed in the content process"
+ );
+ // Feature 2 wasn't explictly marked as needed in the content process, so it is not marked
+ // as disallowed there.
+ is(
+ Services.policies.isAllowed("feature2"),
+ true,
+ "Correctly missing in the content process"
+ );
+ }
+ });
+
+ delete Policies.simple_policy0;
+ delete Policies.simple_policy1;
+ delete Policies.simple_policy2;
+ delete Policies.simple_policy3;
+});
+
+add_task(async function test_policy_cleanup() {
+ await EnterprisePolicyTesting.setupPolicyEngineWithJson("");
+ is(
+ Services.policies.getActivePolicies(),
+ undefined,
+ "No policies should be defined"
+ );
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Engine is inactive at the end of the test"
+ );
+});
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js
new file mode 100644
index 0000000000..a4a274ab08
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js
@@ -0,0 +1,14 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_broken_json() {
+ await setupPolicyEngineWithJson("config_broken_json.json");
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.FAILED,
+ "Engine was correctly set to the error state"
+ );
+});
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js
new file mode 100644
index 0000000000..3da3d250c6
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_enterprise_only.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
+
+add_task(async function test_enterprise_only_policies() {
+ let { Policies } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+
+ let normalPolicyRan = false,
+ enterprisePolicyRan = false;
+
+ Policies.NormalPolicy = {
+ onProfileAfterChange(manager, param) {
+ normalPolicyRan = true;
+ },
+ };
+
+ Policies.EnterpriseOnlyPolicy = {
+ onProfileAfterChange(manager, param) {
+ enterprisePolicyRan = true;
+ },
+ };
+
+ Services.prefs.setBoolPref(PREF_DISALLOW_ENTERPRISE, true);
+
+ await setupPolicyEngineWithJson(
+ // policies.json
+ {
+ policies: {
+ NormalPolicy: true,
+ EnterpriseOnlyPolicy: true,
+ },
+ },
+
+ // custom schema
+ {
+ properties: {
+ NormalPolicy: {
+ type: "boolean",
+ },
+
+ EnterpriseOnlyPolicy: {
+ type: "boolean",
+ enterprise_only: true,
+ },
+ },
+ }
+ );
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Engine is active"
+ );
+ is(normalPolicyRan, true, "Normal policy ran as expected");
+ is(
+ enterprisePolicyRan,
+ false,
+ "Enterprise-only policy was prevented from running"
+ );
+
+ // Clean-up
+ delete Policies.NormalPolicy;
+ delete Policies.EnterpriseOnlyPolicy;
+ Services.prefs.clearUserPref(PREF_DISALLOW_ENTERPRISE);
+});
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_gpo.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_gpo.js
new file mode 100644
index 0000000000..decf158d45
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_gpo.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function setup_preferences() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.policies.alternateGPO", "SOFTWARE\\Mozilla\\PolicyTesting"],
+ ],
+ });
+});
+
+add_task(async function test_gpo_policies() {
+ let { Policies } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+
+ let gpoPolicyRan = false;
+
+ Policies.gpo_policy = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was in the registry");
+ gpoPolicyRan = true;
+ },
+ };
+
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ let regLocation =
+ "SOFTWARE\\Mozilla\\PolicyTesting\\Mozilla\\" + Services.appinfo.name;
+ wrk.create(wrk.ROOT_KEY_CURRENT_USER, regLocation, wrk.ACCESS_WRITE);
+ wrk.writeIntValue("gpo_policy", 1);
+ wrk.close();
+
+ await setupPolicyEngineWithJson(
+ // empty policies.json since we are using GPO
+ {
+ policies: {},
+ },
+
+ // custom schema
+ {
+ properties: {
+ gpo_policy: {
+ type: "boolean",
+ },
+ },
+ }
+ );
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Engine is active"
+ );
+
+ ok(gpoPolicyRan, "GPO Policy ran correctly though onProfileAfterChange");
+
+ delete Policies.gpo_policy;
+
+ wrk.open(wrk.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla", wrk.ACCESS_WRITE);
+ wrk.removeChild("PolicyTesting\\Mozilla\\" + Services.appinfo.name);
+ wrk.removeChild("PolicyTesting\\Mozilla");
+ wrk.removeChild("PolicyTesting");
+ wrk.close();
+});
+
+add_task(async function test_gpo_json_policies() {
+ let { Policies } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+
+ let gpoPolicyRan = false;
+ let jsonPolicyRan = false;
+ let coexistPolicyRan = false;
+
+ Policies.gpo_policy = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was in the registry");
+ gpoPolicyRan = true;
+ },
+ };
+ Policies.json_policy = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was in the JSON");
+ jsonPolicyRan = true;
+ },
+ };
+ Policies.coexist_policy = {
+ onProfileAfterChange(manager, param) {
+ is(param, false, "Param matches what was in the registry (over JSON)");
+ coexistPolicyRan = true;
+ },
+ };
+
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ let regLocation =
+ "SOFTWARE\\Mozilla\\PolicyTesting\\Mozilla\\" + Services.appinfo.name;
+ wrk.create(wrk.ROOT_KEY_CURRENT_USER, regLocation, wrk.ACCESS_WRITE);
+ wrk.writeIntValue("gpo_policy", 1);
+ wrk.writeIntValue("coexist_policy", 0);
+ wrk.close();
+
+ await setupPolicyEngineWithJson(
+ {
+ policies: {
+ json_policy: true,
+ coexist_policy: true,
+ },
+ },
+
+ // custom schema
+ {
+ properties: {
+ gpo_policy: {
+ type: "boolean",
+ },
+ json_policy: {
+ type: "boolean",
+ },
+ coexist_policy: {
+ type: "boolean",
+ },
+ },
+ }
+ );
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Engine is active"
+ );
+
+ ok(gpoPolicyRan, "GPO Policy ran correctly though onProfileAfterChange");
+ ok(jsonPolicyRan, "JSON Policy ran correctly though onProfileAfterChange");
+ ok(
+ coexistPolicyRan,
+ "Coexist Policy ran correctly though onProfileAfterChange"
+ );
+
+ delete Policies.gpo_policy;
+ delete Policies.json_policy;
+ delete Policies.coexist_policy;
+
+ wrk.open(wrk.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla", wrk.ACCESS_WRITE);
+ wrk.removeChild("PolicyTesting\\Mozilla\\" + Services.appinfo.name);
+ wrk.removeChild("PolicyTesting\\Mozilla");
+ wrk.removeChild("PolicyTesting");
+ wrk.close();
+});
+
+add_task(async function test_gpo_broken_json_policies() {
+ let { Policies } = ChromeUtils.importESModule(
+ "resource:///modules/policies/Policies.sys.mjs"
+ );
+
+ let gpoPolicyRan = false;
+
+ Policies.gpo_policy = {
+ onProfileAfterChange(manager, param) {
+ is(param, true, "Param matches what was in the registry");
+ gpoPolicyRan = true;
+ },
+ };
+
+ let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ let regLocation =
+ "SOFTWARE\\Mozilla\\PolicyTesting\\Mozilla\\" + Services.appinfo.name;
+ wrk.create(wrk.ROOT_KEY_CURRENT_USER, regLocation, wrk.ACCESS_WRITE);
+ wrk.writeIntValue("gpo_policy", 1);
+ wrk.close();
+
+ await setupPolicyEngineWithJson(
+ "config_broken_json.json",
+ // custom schema
+ {
+ properties: {
+ gpo_policy: {
+ type: "boolean",
+ },
+ },
+ }
+ );
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.ACTIVE,
+ "Engine is active"
+ );
+
+ ok(gpoPolicyRan, "GPO Policy ran correctly though onProfileAfterChange");
+
+ delete Policies.gpo_policy;
+
+ wrk.open(wrk.ROOT_KEY_CURRENT_USER, "SOFTWARE\\Mozilla", wrk.ACCESS_WRITE);
+ wrk.removeChild("PolicyTesting\\Mozilla\\" + Services.appinfo.name);
+ wrk.removeChild("PolicyTesting\\Mozilla");
+ wrk.removeChild("PolicyTesting");
+ wrk.close();
+});
diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_mistyped_json.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_mistyped_json.js
new file mode 100644
index 0000000000..0b82a11377
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_mistyped_json.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_json_with_mistyped_policies() {
+ // Note: The "polcies" string is intentionally mistyped
+ await setupPolicyEngineWithJson({
+ polcies: {},
+ });
+
+ is(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.FAILED,
+ "Engine was correctly set to the error state"
+ );
+});
diff --git a/toolkit/components/enterprisepolicies/tests/browser/config_broken_json.json b/toolkit/components/enterprisepolicies/tests/browser/config_broken_json.json
new file mode 100644
index 0000000000..7e13efdd88
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/config_broken_json.json
@@ -0,0 +1,3 @@
+{
+ "policies
+}
diff --git a/toolkit/components/enterprisepolicies/tests/browser/head.js b/toolkit/components/enterprisepolicies/tests/browser/head.js
new file mode 100644
index 0000000000..baf0d7780d
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/head.js
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { EnterprisePolicyTesting, PoliciesPrefTracker } =
+ ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+ );
+
+PoliciesPrefTracker.start();
+
+async function setupPolicyEngineWithJson(json, customSchema) {
+ PoliciesPrefTracker.restoreDefaultValues();
+ if (typeof json != "object") {
+ let filePath = getTestFilePath(json ? json : "non-existing-file.json");
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(
+ filePath,
+ customSchema
+ );
+ }
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema);
+}
diff --git a/toolkit/components/enterprisepolicies/tests/moz.build b/toolkit/components/enterprisepolicies/tests/moz.build
new file mode 100644
index 0000000000..14d1ec3803
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+BROWSER_CHROME_MANIFESTS += [
+ "browser/browser.toml",
+]
+
+TESTING_JS_MODULES += [
+ "EnterprisePolicyTesting.sys.mjs",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.toml"]
diff --git a/toolkit/components/enterprisepolicies/tests/xpcshell/head.js b/toolkit/components/enterprisepolicies/tests/xpcshell/head.js
new file mode 100644
index 0000000000..d4a585e299
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/xpcshell/head.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+const { EnterprisePolicyTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/EnterprisePolicyTesting.sys.mjs"
+);
+
+updateAppInfo({
+ name: "XPCShell",
+ ID: "xpcshell@tests.mozilla.org",
+ version: "48",
+ platformVersion: "48",
+});
+
+// This initializes the policy engine for xpcshell tests
+let policies = Cc["@mozilla.org/enterprisepolicies;1"].getService(
+ Ci.nsIObserver
+);
+policies.observe(null, "policies-startup", null);
+
+async function setupPolicyEngineWithJson(json, customSchema) {
+ if (typeof json != "object") {
+ let filePath = do_get_file(json ? json : "non-existing-file.json").path;
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(
+ filePath,
+ customSchema
+ );
+ }
+ return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema);
+}
diff --git a/toolkit/components/enterprisepolicies/tests/xpcshell/test_empty.js b/toolkit/components/enterprisepolicies/tests/xpcshell/test_empty.js
new file mode 100644
index 0000000000..7458f95960
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/xpcshell/test_empty.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function test_empty_toplevel() {
+ await setupPolicyEngineWithJson({
+ policies: {},
+ });
+
+ equal(
+ Services.policies.status,
+ Ci.nsIEnterprisePolicies.INACTIVE,
+ "Engine is not active"
+ );
+});
diff --git a/toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.toml b/toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..7229da98ed
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,6 @@
+[DEFAULT]
+firefox-appdir = "browser"
+head = "head.js"
+skip-if = ["os == 'android'"]
+
+["test_empty.js"]