summaryrefslogtreecommitdiffstats
path: root/toolkit/components/enterprisepolicies
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/enterprisepolicies')
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePolicies.jsm22
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePoliciesContent.jsm27
-rw-r--r--toolkit/components/enterprisepolicies/EnterprisePoliciesParent.jsm656
-rw-r--r--toolkit/components/enterprisepolicies/WindowsGPOParser.jsm111
-rw-r--r--toolkit/components/enterprisepolicies/components.conf16
-rw-r--r--toolkit/components/enterprisepolicies/macOSPoliciesParser.jsm158
-rw-r--r--toolkit/components/enterprisepolicies/moz.build37
-rw-r--r--toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl68
-rw-r--r--toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.jsm184
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser.ini9
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js126
-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_mistyped_json.js17
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/config_broken_json.json3
-rw-r--r--toolkit/components/enterprisepolicies/tests/browser/head.js28
-rw-r--r--toolkit/components/enterprisepolicies/tests/moz.build15
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/head.js47
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/test_empty.js16
-rw-r--r--toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.ini5
20 files changed, 1629 insertions, 0 deletions
diff --git a/toolkit/components/enterprisepolicies/EnterprisePolicies.jsm b/toolkit/components/enterprisepolicies/EnterprisePolicies.jsm
new file mode 100644
index 0000000000..0d615ed560
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePolicies.jsm
@@ -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/. */
+
+var EXPORTED_SYMBOLS = ["EnterprisePolicies"];
+
+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.import(
+ "resource://gre/modules/EnterprisePoliciesParent.jsm"
+ );
+ return new EnterprisePoliciesManager();
+ }
+ const { EnterprisePoliciesManagerContent } = ChromeUtils.import(
+ "resource://gre/modules/EnterprisePoliciesContent.jsm"
+ );
+ return new EnterprisePoliciesManagerContent();
+}
diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.jsm b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.jsm
new file mode 100644
index 0000000000..ba101cf814
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesContent.jsm
@@ -0,0 +1,27 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["EnterprisePoliciesManagerContent"];
+
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+
+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.jsm b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.jsm
new file mode 100644
index 0000000000..8b0a5170cb
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.jsm
@@ -0,0 +1,656 @@
+/* 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/. */
+
+var EXPORTED_SYMBOLS = ["EnterprisePoliciesManager"];
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { AppConstants } = ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.jsm",
+ macOSPoliciesParser:
+ "resource://gre/modules/policies/macOSPoliciesParser.jsm",
+ Policies: "resource:///modules/policies/Policies.jsm",
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.jsm",
+});
+
+// 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, 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";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+ 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.jsm for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+let env = Cc["@mozilla.org/process/environment;1"].getService(
+ Ci.nsIEnvironment
+);
+const isXpcshell = 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;
+}
+
+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 Policies) {
+ let policyImpl = 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) {
+ this.status = Ci.nsIEnterprisePolicies.INACTIVE;
+ return;
+ }
+
+ if (provider.failed) {
+ this.status = Ci.nsIEnterprisePolicies.FAILED;
+ return;
+ }
+
+ this.status = Ci.nsIEnterprisePolicies.ACTIVE;
+ this._parsedPolicies = {};
+ Services.telemetry.scalarSet(
+ "policies.count",
+ Object.keys(provider.policies).length
+ );
+ this._activatePolicies(provider.policies);
+
+ Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true);
+ },
+
+ _chooseProvider() {
+ let provider = null;
+ if (AppConstants.platform == "win") {
+ provider = new WindowsGPOPoliciesProvider();
+ } else if (AppConstants.platform == "macosx") {
+ provider = new macOSPoliciesProvider();
+ }
+ if (provider && provider.hasPolicies) {
+ return provider;
+ }
+
+ provider = new JSONPoliciesProvider();
+ if (provider.hasPolicies) {
+ return provider;
+ }
+
+ return null;
+ },
+
+ _activatePolicies(unparsedPolicies) {
+ let { schema } = ChromeUtils.import(
+ "resource:///modules/policies/schema.jsm"
+ );
+
+ for (let policyName of Object.keys(unparsedPolicies)) {
+ let policySchema = schema.properties[policyName];
+ let policyParameters = unparsedPolicies[policyName];
+
+ if (!policySchema) {
+ log.error(`Unknown policy: ${policyName}`);
+ continue;
+ }
+
+ if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
+ log.error(`Policy ${policyName} is only allowed on ESR`);
+ continue;
+ }
+
+ let {
+ valid: parametersAreValid,
+ parsedValue: parsedParameters,
+ } = JsonSchemaValidator.validate(policyParameters, policySchema, {
+ allowExtraProperties: true,
+ });
+
+ if (!parametersAreValid) {
+ log.error(`Invalid parameters specified for ${policyName}.`);
+ continue;
+ }
+
+ this._parsedPolicies[policyName] = parsedParameters;
+ let policyImpl = Policies[policyName];
+
+ 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) {
+ 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;
+ for (let timing of Object.keys(this._callbacks)) {
+ this._callbacks[timing] = [];
+ }
+
+ let { PromiseUtils } = ChromeUtils.import(
+ "resource://gre/modules/PromiseUtils.jsm"
+ );
+ // 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.
+
+ await PromiseUtils.idleDispatch(() => {
+ this.observe(null, "policies-startup", null);
+ });
+
+ await PromiseUtils.idleDispatch(() => {
+ this.observe(null, "profile-after-change", null);
+ });
+
+ await PromiseUtils.idleDispatch(() => {
+ this.observe(null, "final-ui-startup", null);
+ });
+
+ await PromiseUtils.idleDispatch(() => {
+ this.observe(null, "sessionstore-windows-restored", null);
+ });
+ },
+
+ // 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, Cu.reportError);
+ 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);
+ }
+ return 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;
+ },
+};
+
+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;
+ }
+
+ if (AppConstants.MOZ_UPDATE_CHANNEL != "release") {
+ return true;
+ }
+
+ return false;
+}
+
+/*
+ * 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._failed = false;
+ this._readData();
+ }
+
+ get hasPolicies() {
+ return (
+ this._failed ||
+ (this._policies !== null && !isEmptyObject(this._policies))
+ );
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ return this._failed;
+ }
+
+ _getConfigurationFile() {
+ let configFile = null;
+
+ if (AppConstants.platform == "linux") {
+ let systemConfigFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ systemConfigFile.initWithPath(
+ "/etc/" + Services.appinfo.name.toLowerCase() + "/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) {
+ this._policies = JSON.parse(data).policies;
+
+ if (!this._policies) {
+ 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) {
+ log.error("Error parsing JSON file");
+ this._failed = true;
+ } else {
+ log.error("Error reading file");
+ 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.
+ log.debug("root = HKEY_CURRENT_USER");
+ this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
+ log.debug("root = HKEY_LOCAL_MACHINE");
+ 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 {
+ wrk.open(root, "SOFTWARE\\Policies", wrk.ACCESS_READ);
+ if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
+ this._policies = WindowsGPOParser.readPolicies(wrk, this._policies);
+ }
+ wrk.close();
+ } catch (e) {
+ 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 = macOSPoliciesParser.readPolicies(prefReader);
+ }
+
+ get hasPolicies() {
+ return this._policies !== null && Object.keys(this._policies).length;
+ }
+
+ get policies() {
+ return this._policies;
+ }
+
+ get failed() {
+ return this._failed;
+ }
+}
diff --git a/toolkit/components/enterprisepolicies/WindowsGPOParser.jsm b/toolkit/components/enterprisepolicies/WindowsGPOParser.jsm
new file mode 100644
index 0000000000..2b945ab90c
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/WindowsGPOParser.jsm
@@ -0,0 +1,111 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+ 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.jsm for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+var EXPORTED_SYMBOLS = ["WindowsGPOParser"];
+
+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) {
+ 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 (log._maxLogLevel == "debug") {
+ 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);
+ 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
+ return wrk.readStringValue(value).replace(/\0/g, "\n");
+ 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..6f0fac6bf7
--- /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'],
+ 'jsm': 'resource://gre/modules/EnterprisePolicies.jsm',
+ 'constructor': 'EnterprisePolicies',
+ },
+]
diff --git a/toolkit/components/enterprisepolicies/macOSPoliciesParser.jsm b/toolkit/components/enterprisepolicies/macOSPoliciesParser.jsm
new file mode 100644
index 0000000000..dac5469584
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/macOSPoliciesParser.jsm
@@ -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/. */
+
+"use strict";
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+const PREF_LOGLEVEL = "browser.policies.loglevel";
+
+XPCOMUtils.defineLazyGetter(this, "log", () => {
+ let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
+ 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.jsm for details.
+ maxLogLevel: "error",
+ maxLogLevelPref: PREF_LOGLEVEL,
+ });
+});
+
+var EXPORTED_SYMBOLS = ["macOSPoliciesParser"];
+
+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 (log.maxLogLevel == "debug") {
+ log.debug(JSON.stringify(nativePolicies, null, 2));
+ }
+
+ return nativePolicies;
+ },
+
+ removeUnknownPolicies(policies) {
+ let { schema } = ChromeUtils.import(
+ "resource:///modules/policies/schema.jsm"
+ );
+
+ for (let policyName of Object.keys(policies)) {
+ if (!schema.properties.hasOwnProperty(policyName)) {
+ 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;
+ }
+
+ 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)) {
+ 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)) {
+ 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..09d2046e1b
--- /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.jsm",
+ "EnterprisePoliciesContent.jsm",
+ "EnterprisePoliciesParent.jsm",
+ ]
+
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ EXTRA_JS_MODULES.policies += [
+ "WindowsGPOParser.jsm",
+ ]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ EXTRA_JS_MODULES.policies += [
+ "macOSPoliciesParser.jsm",
+ ]
diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl
new file mode 100644
index 0000000000..fd81f5a9da
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl
@@ -0,0 +1,68 @@
+/* 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 whitelist, blacklist and settings to determine if an extension
+ * may be installed.
+ *
+ * @returns A boolean - true of the extension may be installed.
+ */
+ bool mayInstallAddon(in jsval addon);
+
+ /**
+ * Uses install_sources to determine if an extension can be installed
+ * from the given URI.
+ *
+ * @returns A boolean - true of the extension may be installed.
+ */
+ bool allowedInstallSource(in nsIURI uri);
+};
diff --git a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.jsm b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.jsm
new file mode 100644
index 0000000000..dd98c03bce
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.jsm
@@ -0,0 +1,184 @@
+/* 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 { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm");
+ChromeUtils.defineModuleGetter(
+ this,
+ "FileTestUtils",
+ "resource://testing-common/FileTestUtils.jsm"
+);
+
+var EXPORTED_SYMBOLS = ["EnterprisePolicyTesting", "PoliciesPrefTracker"];
+
+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 = FileTestUtils.getTempFile("policies.json").path;
+
+ // This file gets automatically deleted by FileTestUtils
+ // at the end of the test run.
+ await OS.File.writeAtomic(filePath, JSON.stringify(json), {
+ encoding: "utf-8",
+ });
+ } 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
+ Cu.unload("resource:///modules/policies/schema.jsm");
+
+ if (customSchema) {
+ let schemaModule = ChromeUtils.import(
+ "resource:///modules/policies/schema.jsm",
+ null
+ );
+ schemaModule.schema = customSchema;
+ }
+
+ 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.
+ */
+var PoliciesPrefTracker = {
+ _originalFunc: null,
+ _originalValues: new Map(),
+
+ start() {
+ let PoliciesBackstage = ChromeUtils.import(
+ "resource:///modules/policies/Policies.jsm",
+ null
+ );
+ this._originalFunc = PoliciesBackstage.setDefaultPref;
+ PoliciesBackstage.setDefaultPref = this.hoistedSetDefaultPref.bind(this);
+ },
+
+ stop() {
+ this.restoreDefaultValues();
+
+ let PoliciesBackstage = ChromeUtils.import(
+ "resource:///modules/policies/Policies.jsm",
+ null
+ );
+ PoliciesBackstage.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.ini b/toolkit/components/enterprisepolicies/tests/browser/browser.ini
new file mode 100644
index 0000000000..23a860dd8b
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser.ini
@@ -0,0 +1,9 @@
+[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_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..24de9c2f23
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js
@@ -0,0 +1,126 @@
+/* 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.import(
+ "resource:///modules/policies/Policies.jsm"
+ );
+
+ 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;
+});
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..85bec8191f
--- /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.import(
+ "resource:///modules/policies/Policies.jsm"
+ );
+
+ 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_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..f099749df1
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/browser/head.js
@@ -0,0 +1,28 @@
+/* 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.import(
+ "resource://testing-common/EnterprisePolicyTesting.jsm",
+ null
+);
+const { TestUtils } = ChromeUtils.import(
+ "resource://testing-common/TestUtils.jsm",
+ null
+);
+
+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..63b6391244
--- /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.ini",
+]
+
+TESTING_JS_MODULES += [
+ "EnterprisePolicyTesting.jsm",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["xpcshell/xpcshell.ini"]
diff --git a/toolkit/components/enterprisepolicies/tests/xpcshell/head.js b/toolkit/components/enterprisepolicies/tests/xpcshell/head.js
new file mode 100644
index 0000000000..74bd1252f9
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/xpcshell/head.js
@@ -0,0 +1,47 @@
+/* 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 { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
+const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
+const { Preferences } = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm"
+);
+const { updateAppInfo, getAppInfo } = ChromeUtils.import(
+ "resource://testing-common/AppInfo.jsm"
+);
+const { FileTestUtils } = ChromeUtils.import(
+ "resource://testing-common/FileTestUtils.jsm"
+);
+const { PermissionTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PermissionTestUtils.jsm"
+);
+const { EnterprisePolicyTesting } = ChromeUtils.import(
+ "resource://testing-common/EnterprisePolicyTesting.jsm"
+);
+
+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.ini b/toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..b5ba816b26
--- /dev/null
+++ b/toolkit/components/enterprisepolicies/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,5 @@
+[DEFAULT]
+head = head.js
+skip-if = toolkit == 'android'
+
+[test_empty.js]