diff --git a/.eslintignore b/.eslintignore index 89b5e01c1ba4..be7ca64405f6 100644 --- a/.eslintignore +++ b/.eslintignore @@ -77,6 +77,10 @@ browser/base/content/newtab/** # Test files that are really json not js, and don't need to be linted. browser/components/sessionstore/test/unit/data/sessionstore_valid.js browser/components/sessionstore/test/unit/data/sessionstore_invalid.js +# This file is split into two in order to keep it as a valid json file +# for documentation purposes (policies.json) but to be accessed by the +# code as a .jsm (schema.jsm) +browser/components/enterprisepolicies/schemas/schema.jsm # generated & special files in cld2 browser/components/translation/cld2/** # Screenshots and Follow-on search are imported as a system add-on and have diff --git a/browser/components/enterprisepolicies/EnterprisePolicies.js b/browser/components/enterprisepolicies/EnterprisePolicies.js new file mode 100644 index 000000000000..ba1189c25f94 --- /dev/null +++ b/browser/components/enterprisepolicies/EnterprisePolicies.js @@ -0,0 +1,358 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); +Cu.import("resource://gre/modules/AppConstants.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + NetUtil: "resource://gre/modules/NetUtil.jsm", + Policies: "resource:///modules/policies/Policies.jsm", + PoliciesValidator: "resource:///modules/policies/PoliciesValidator.jsm", +}); + +// This is the file that will be searched for in the +// ${InstallDir}/distribution folder. +const POLICIES_FILENAME = "policies.json"; + +// 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"; + +// This pref is meant to be temporary: it will only be used while we're +// testing this feature without rolling it out officially. When the +// policy engine is released, this pref should be removed. +const PREF_ENABLED = "browser.policies.enabled"; +const PREF_LOGLEVEL = "browser.policies.loglevel"; + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = Cu.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, + }); +}); + +// ==== Start XPCOM Boilerplate ==== \\ + +// Factory object +const EnterprisePoliciesFactory = { + _instance: null, + createInstance: function BGSF_createInstance(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return this._instance == null ? + this._instance = new EnterprisePoliciesManager() : this._instance; + } +}; + +// ==== End XPCOM Boilerplate ==== // + +// Constructor +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 = { + // for XPCOM + classID: Components.ID("{ea4e1414-779b-458b-9d1f-d18e8efbc145}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, + Ci.nsISupportsWeakReference, + Ci.nsIEnterprisePolicies]), + + // redefine the default factory for XPCOMUtils + _xpcom_factory: EnterprisePoliciesFactory, + + _initialize() { + if (!Services.prefs.getBoolPref(PREF_ENABLED, false)) { + this.status = Ci.nsIEnterprisePolicies.INACTIVE; + return; + } + + this._file = new JSONFileReader(getConfigurationFile()); + this._file.readData(); + + if (!this._file.exists) { + this.status = Ci.nsIEnterprisePolicies.INACTIVE; + return; + } + + if (this._file.failed) { + this.status = Ci.nsIEnterprisePolicies.FAILED; + return; + } + + this.status = Ci.nsIEnterprisePolicies.ACTIVE; + this._activatePolicies(); + }, + + _activatePolicies() { + let { schema } = Cu.import("resource:///modules/policies/schema.jsm", {}); + let json = this._file.json; + + for (let policyName of Object.keys(json.policies)) { + let policySchema = schema.properties[policyName]; + let policyParameters = json.policies[policyName]; + + if (!policySchema) { + log.error(`Unknown policy: ${policyName}`); + continue; + } + + let [parametersAreValid, parsedParameters] = + PoliciesValidator.validateAndParseParameters(policyParameters, + policySchema); + + if (!parametersAreValid) { + log.error(`Invalid parameters specified for ${policyName}.`); + continue; + } + + let policyImpl = Policies[policyName]; + + for (let timing of Object.keys(this._callbacks)) { + let policyCallback = policyImpl["on" + timing]; + if (policyCallback) { + this._schedulePolicyCallback( + timing, + policyCallback.bind(null, + this, /* the EnterprisePoliciesManager */ + parsedParameters)); + } + } + } + }, + + _callbacks: { + BeforeAddons: [], + ProfileAfterChange: [], + BeforeUIStartup: [], + AllWindowsRestored: [], + }, + + _schedulePolicyCallback(timing, callback) { + this._callbacks[timing].push(callback); + }, + + _runPoliciesCallbacks(timing) { + let callbacks = this._callbacks[timing]; + while (callbacks.length > 0) { + let callback = callbacks.shift(); + try { + callback(); + } catch (ex) { + log.error("Error running ", callback, `for ${timing}:`, ex); + } + } + }, + + async _restart() { + if (!Cu.isInAutomation) { + return; + } + + DisallowedFeatures = {}; + + this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED; + for (let timing of Object.keys(this._callbacks)) { + this._callbacks[timing] = []; + } + delete Services.ppmm.initialProcessData.policies; + Services.ppmm.broadcastAsyncMessage("EnterprisePolicies:Restart", null); + + let { PromiseUtils } = Cu.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": + this._initialize(); + this._runPoliciesCallbacks("BeforeAddons"); + break; + + case "profile-after-change": + // Before the first set of policy callbacks runs, we must + // initialize the service. + this._runPoliciesCallbacks("ProfileAfterChange"); + break; + + case "final-ui-startup": + this._runPoliciesCallbacks("BeforeUIStartup"); + break; + + case "sessionstore-windows-restored": + this._runPoliciesCallbacks("AllWindowsRestored"); + + // 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] = true; + + // NOTE: For optimization purposes, only features marked as needed + // on content process will be passed onto the child processes. + if (neededOnContentProcess) { + Services.ppmm.initialProcessData.policies + .disallowedFeatures.push(feature); + + if (Services.ppmm.childCount > 1) { + // If there has been a content process already initialized, let's + // broadcast the newly disallowed feature. + Services.ppmm.broadcastAsyncMessage( + "EnterprisePolicies:DisallowFeature", {feature} + ); + } + } + }, + + // ------------------------------ + // public nsIEnterprisePolicies members + // ------------------------------ + + _status: Ci.nsIEnterprisePolicies.UNINITIALIZED, + + set status(val) { + this._status = val; + if (val != Ci.nsIEnterprisePolicies.INACTIVE) { + Services.ppmm.initialProcessData.policies = { + status: val, + disallowedFeatures: [], + }; + } + return val; + }, + + get status() { + return this._status; + }, + + isAllowed: function BG_sanitize(feature) { + return !(feature in DisallowedFeatures); + }, +}; + +let DisallowedFeatures = {}; + +function JSONFileReader(file) { + this._file = file; + this._data = { + exists: null, + failed: false, + json: null, + }; +} + +JSONFileReader.prototype = { + get exists() { + if (this._data.exists === null) { + this.readData(); + } + + return this._data.exists; + }, + + get failed() { + return this._data.failed; + }, + + get json() { + if (this._data.failed) { + return null; + } + + if (this._data.json === null) { + this.readData(); + } + + return this._data.json; + }, + + readData() { + try { + let data = Cu.readUTF8File(this._file); + if (data) { + this._data.exists = true; + this._data.json = JSON.parse(data); + } else { + this._data.exists = false; + } + } catch (ex) { + if (ex instanceof Components.Exception && + ex.result == Cr.NS_ERROR_FILE_NOT_FOUND) { + this._data.exists = false; + } else if (ex instanceof SyntaxError) { + log.error("Error parsing JSON file"); + this._data.failed = true; + } else { + log.error("Error reading file"); + this._data.failed = true; + } + } + } +}; + +function getConfigurationFile() { + let configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile); + configFile.append(POLICIES_FILENAME); + + let prefType = Services.prefs.getPrefType(PREF_ALTERNATE_PATH); + + if ((prefType == Services.prefs.PREF_STRING) && !configFile.exists()) { + // We only want to use the alternate file path if the file on the install + // folder doesn't exist. Otherwise it'd be possible for a user to override + // the admin-provided policies by changing the user-controlled prefs. + // This pref is only meant for tests, so it's fine to use this extra + // synchronous configFile.exists() above. + configFile = Cc["@mozilla.org/file/local;1"] + .createInstance(Ci.nsIFile); + let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH); + configFile.initWithPath(alternatePath); + } + + return configFile; +} + +var components = [EnterprisePoliciesManager]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/components/enterprisepolicies/EnterprisePolicies.manifest b/browser/components/enterprisepolicies/EnterprisePolicies.manifest new file mode 100644 index 000000000000..571ae1362996 --- /dev/null +++ b/browser/components/enterprisepolicies/EnterprisePolicies.manifest @@ -0,0 +1,5 @@ +component {ea4e1414-779b-458b-9d1f-d18e8efbc145} EnterprisePolicies.js process=main +contract @mozilla.org/browser/enterprisepolicies;1 {ea4e1414-779b-458b-9d1f-d18e8efbc145} process=main + +component {dc6358f8-d167-4566-bf5b-4350b5e6a7a2} EnterprisePoliciesContent.js process=content +contract @mozilla.org/browser/enterprisepolicies;1 {dc6358f8-d167-4566-bf5b-4350b5e6a7a2} process=content diff --git a/browser/components/enterprisepolicies/EnterprisePoliciesContent.js b/browser/components/enterprisepolicies/EnterprisePoliciesContent.js new file mode 100644 index 000000000000..bc9bda2f8c13 --- /dev/null +++ b/browser/components/enterprisepolicies/EnterprisePoliciesContent.js @@ -0,0 +1,91 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PREF_LOGLEVEL = "browser.policies.loglevel"; + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); + return new ConsoleAPI({ + prefix: "Enterprise Policies Child", + // 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, + }); +}); + + +// ==== Start XPCOM Boilerplate ==== \\ + +// Factory object +const EnterprisePoliciesFactory = { + _instance: null, + createInstance: function BGSF_createInstance(outer, iid) { + if (outer != null) + throw Components.results.NS_ERROR_NO_AGGREGATION; + return this._instance == null ? + this._instance = new EnterprisePoliciesManagerContent() : this._instance; + } +}; + +// ==== End XPCOM Boilerplate ==== // + + +function EnterprisePoliciesManagerContent() { + let policies = Services.cpmm.initialProcessData.policies; + if (policies) { + this._status = policies.status; + // make a copy of the array so that we can keep adding to it + // in a way that is not confusing. + this._disallowedFeatures = policies.disallowedFeatures.slice(); + } + + Services.cpmm.addMessageListener("EnterprisePolicies:DisallowFeature", this); + Services.cpmm.addMessageListener("EnterprisePolicies:Restart", this); +} + +EnterprisePoliciesManagerContent.prototype = { + // for XPCOM + classID: Components.ID("{dc6358f8-d167-4566-bf5b-4350b5e6a7a2}"), + QueryInterface: XPCOMUtils.generateQI([Ci.nsIMessageListener, + Ci.nsIEnterprisePolicies]), + + // redefine the default factory for XPCOMUtils + _xpcom_factory: EnterprisePoliciesFactory, + + _status: Ci.nsIEnterprisePolicies.INACTIVE, + + _disallowedFeatures: [], + + receiveMessage({name, data}) { + switch (name) { + case "EnterprisePolicies:DisallowFeature": + this._disallowedFeatures.push(data.feature); + break; + + case "EnterprisePolicies:Restart": + this._disallowedFeatures = []; + break; + } + }, + + get status() { + return this._status; + }, + + isAllowed(feature) { + return !this._disallowedFeatures.includes(feature); + } +}; + +var components = [EnterprisePoliciesManagerContent]; +this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components); diff --git a/browser/components/enterprisepolicies/Policies.jsm b/browser/components/enterprisepolicies/Policies.jsm new file mode 100644 index 000000000000..ab1531bbf322 --- /dev/null +++ b/browser/components/enterprisepolicies/Policies.jsm @@ -0,0 +1,38 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PREF_LOGLEVEL = "browser.policies.loglevel"; + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); + return new ConsoleAPI({ + prefix: "Policies.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, + }); +}); + +this.EXPORTED_SYMBOLS = ["Policies"]; + +this.Policies = { + "block_about_config": { + onBeforeUIStartup(manager, param) { + if (param == true) { + manager.disallowFeature("about:config", true); + } + } + }, +}; diff --git a/browser/components/enterprisepolicies/PoliciesValidator.jsm b/browser/components/enterprisepolicies/PoliciesValidator.jsm new file mode 100644 index 000000000000..cfbb20f6e3b0 --- /dev/null +++ b/browser/components/enterprisepolicies/PoliciesValidator.jsm @@ -0,0 +1,148 @@ +/* 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 Ci = Components.interfaces; +const Cc = Components.classes; +const Cr = Components.results; +const Cu = Components.utils; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +Cu.import("resource://gre/modules/Services.jsm"); + +const PREF_LOGLEVEL = "browser.policies.loglevel"; + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let { ConsoleAPI } = Cu.import("resource://gre/modules/Console.jsm", {}); + return new ConsoleAPI({ + prefix: "PoliciesValidator.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, + }); +}); + +this.EXPORTED_SYMBOLS = ["PoliciesValidator"]; + +this.PoliciesValidator = { + validateAndParseParameters(param, properties) { + return validateAndParseParamRecursive(param, properties); + } +}; + +function validateAndParseParamRecursive(param, properties) { + if (properties.enum) { + if (properties.enum.includes(param)) { + return [true, param]; + } + return [false, null]; + } + + log.debug(`checking @${param}@ for type ${properties.type}`); + switch (properties.type) { + case "boolean": + case "number": + case "integer": + case "string": + case "URL": + case "origin": + return validateAndParseSimpleParam(param, properties.type); + + case "array": + if (!Array.isArray(param)) { + log.error("Array expected but not received"); + return [false, null]; + } + + let parsedArray = []; + for (let item of param) { + log.debug(`in array, checking @${item}@ for type ${properties.items.type}`); + let [valid, parsedValue] = validateAndParseParamRecursive(item, properties.items); + if (!valid) { + return [false, null]; + } + + parsedArray.push(parsedValue); + } + + return [true, parsedArray]; + + case "object": { + if (typeof(param) != "object") { + log.error("Object expected but not received"); + return [false, null]; + } + + let parsedObj = {}; + for (let property of Object.keys(properties.properties)) { + log.debug(`in object, for property ${property} checking @${param[property]}@ for type ${properties.properties[property].type}`); + let [valid, parsedValue] = validateAndParseParamRecursive(param[property], properties.properties[property]); + if (!valid) { + return [false, null]; + } + + parsedObj[property] = parsedValue; + } + + return [true, parsedObj]; + } + } + + return [false, null]; +} + +function validateAndParseSimpleParam(param, type) { + let valid = false; + let parsedParam = param; + + switch (type) { + case "boolean": + case "number": + case "string": + valid = (typeof(param) == type); + break; + + // integer is an alias to "number" that some JSON schema tools use + case "integer": + valid = (typeof(param) == "number"); + break; + + case "origin": + if (typeof(param) != "string") { + break; + } + + try { + parsedParam = Services.io.newURI(param); + + let pathQueryRef = parsedParam.pathQueryRef; + // Make sure that "origin" types won't accept full URLs. + if (pathQueryRef != "/" && pathQueryRef != "") { + valid = false; + } else { + valid = true; + } + } catch (ex) { + valid = false; + } + break; + + case "URL": + if (typeof(param) != "string") { + break; + } + + try { + parsedParam = Services.io.newURI(param); + valid = true; + } catch (ex) { + valid = false; + } + break; + } + + return [valid, parsedParam]; +} diff --git a/browser/components/enterprisepolicies/helpers/moz.build b/browser/components/enterprisepolicies/helpers/moz.build new file mode 100644 index 000000000000..560d837fc699 --- /dev/null +++ b/browser/components/enterprisepolicies/helpers/moz.build @@ -0,0 +1,8 @@ +# -*- 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") diff --git a/browser/components/enterprisepolicies/helpers/sample.json b/browser/components/enterprisepolicies/helpers/sample.json new file mode 100644 index 000000000000..ab503c3ffe4b --- /dev/null +++ b/browser/components/enterprisepolicies/helpers/sample.json @@ -0,0 +1,18 @@ +{ + "policies": { + "block_about_config": true, + "dont_check_default_browser": true, + + "flash_plugin": { + "allow": [ + "https://www.example.com" + ], + + "block": [ + "https://www.example.org" + ] + }, + + "block_about_profiles": true + } +} diff --git a/browser/components/enterprisepolicies/moz.build b/browser/components/enterprisepolicies/moz.build new file mode 100644 index 000000000000..b4cea960f5b0 --- /dev/null +++ b/browser/components/enterprisepolicies/moz.build @@ -0,0 +1,30 @@ +# -*- 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") + +DIRS += [ + 'helpers', + 'schemas', +] + +TEST_DIRS += [ + 'tests' +] + +EXTRA_COMPONENTS += [ + 'EnterprisePolicies.js', + 'EnterprisePolicies.manifest', + 'EnterprisePoliciesContent.js', +] + +EXTRA_JS_MODULES.policies += [ + 'Policies.jsm', + 'PoliciesValidator.jsm', +] + +FINAL_LIBRARY = 'browsercomps' diff --git a/browser/components/enterprisepolicies/schemas/configuration.json b/browser/components/enterprisepolicies/schemas/configuration.json new file mode 100644 index 000000000000..8d3e9e43c25c --- /dev/null +++ b/browser/components/enterprisepolicies/schemas/configuration.json @@ -0,0 +1,10 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "policies": { + "$ref": "policies.json" + } + }, + "required": ["policies"] +} diff --git a/browser/components/enterprisepolicies/schemas/moz.build b/browser/components/enterprisepolicies/schemas/moz.build new file mode 100644 index 000000000000..54c07f173169 --- /dev/null +++ b/browser/components/enterprisepolicies/schemas/moz.build @@ -0,0 +1,12 @@ +# -*- 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") + +EXTRA_PP_JS_MODULES.policies += [ + 'schema.jsm', +] diff --git a/browser/components/enterprisepolicies/schemas/policies-schema.json b/browser/components/enterprisepolicies/schemas/policies-schema.json new file mode 100644 index 000000000000..c0257dd93fd6 --- /dev/null +++ b/browser/components/enterprisepolicies/schemas/policies-schema.json @@ -0,0 +1,13 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "block_about_config": { + "description": "Blocks access to the about:config page.", + "first_available": "60.0", + + "type": "boolean", + "enum": [true] + } + } +} diff --git a/browser/components/enterprisepolicies/schemas/schema.jsm b/browser/components/enterprisepolicies/schemas/schema.jsm new file mode 100644 index 000000000000..3624bfd0344c --- /dev/null +++ b/browser/components/enterprisepolicies/schemas/schema.jsm @@ -0,0 +1,10 @@ +/* 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"; + +this.EXPORTED_SYMBOLS = ["schema"]; + +this.schema = +#include policies-schema.json diff --git a/browser/components/enterprisepolicies/tests/browser/.eslintrc.js b/browser/components/enterprisepolicies/tests/browser/.eslintrc.js new file mode 100644 index 000000000000..b1c842d8a6db --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "plugin:mozilla/browser-test" + ] +}; diff --git a/browser/components/enterprisepolicies/tests/browser/browser.ini b/browser/components/enterprisepolicies/tests/browser/browser.ini new file mode 100644 index 000000000000..28ef70bff8b9 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +prefs = + browser.policies.enabled=true +support-files = + head.js + config_simple_policies.json + config_broken_json.json + +[browser_policies_broken_json.js] +[browser_policies_simple_policies.js] +[browser_policies_validate_and_parse_API.js] diff --git a/browser/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js b/browser/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js new file mode 100644 index 000000000000..7d1f5d6f3c68 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_broken_json.js @@ -0,0 +1,15 @@ +/* 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"; + +add_task(async function test_clean_slate() { + await startWithCleanSlate(); +}); + +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/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_policies.js b/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_policies.js new file mode 100644 index 000000000000..55f368c2fff6 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_simple_policies.js @@ -0,0 +1,101 @@ +/* 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"; + +add_task(async function test_clean_slate() { + await startWithCleanSlate(); +}); + +add_task(async function test_simple_policies() { + await ContentTask.spawn(gBrowser.selectedBrowser, null, async function() { + // Initialize the service in the content process, in case it hasn't + // already started. + Services.policies; + }); + + let { Policies } = Cu.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( + "config_simple_policies.json", + /* 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 ContentTask.spawn(gBrowser.selectedBrowser, null, 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/browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js b/browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js new file mode 100644 index 000000000000..4dc81acc4b74 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_validate_and_parse_API.js @@ -0,0 +1,231 @@ +/* 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"; + +/* This file will test the parameters parsing and validation directly through + the PoliciesValidator API. + */ + +const { PoliciesValidator } = Cu.import("resource:///modules/policies/PoliciesValidator.jsm", {}); + +add_task(async function test_boolean_values() { + let schema = { + type: "boolean" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters(true, schema); + ok(valid && parsed === true, "Parsed boolean value correctly"); + + [valid, parsed] = PoliciesValidator.validateAndParseParameters(false, schema); + ok(valid && parsed === false, "Parsed boolean value correctly"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters("0", schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters("true", schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value"); +}); + +add_task(async function test_number_values() { + let schema = { + type: "number" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema); + ok(valid && parsed === 1, "Parsed number value correctly"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value"); +}); + +add_task(async function test_integer_values() { + // Integer is an alias for number + let schema = { + type: "integer" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters(1, schema); + ok(valid && parsed == 1, "Parsed integer value correctly"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters("1", schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value"); +}); + +add_task(async function test_string_values() { + let schema = { + type: "string" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters("foobar", schema); + ok(valid && parsed == "foobar", "Parsed string value correctly"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters(1, schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters(true, schema)[0], "No type coercion"); + ok(!PoliciesValidator.validateAndParseParameters(undefined, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); + ok(!PoliciesValidator.validateAndParseParameters(null, schema)[0], "Invalid value"); +}); + +add_task(async function test_URL_values() { + let schema = { + type: "URL" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com/foo#bar", schema); + ok(valid, "URL is valid"); + ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI"); + is(parsed.prePath, "https://www.example.com", "prePath is correct"); + is(parsed.pathQueryRef, "/foo#bar", "pathQueryRef is correct"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters("www.example.com", schema)[0], "Scheme is required for URL"); + ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid URL"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); +}); + +add_task(async function test_origin_values() { + // Origin is a URL that doesn't contain a path/query string (i.e., it's only scheme + host + port) + let schema = { + type: "origin" + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters("https://www.example.com", schema); + ok(valid, "Origin is valid"); + ok(parsed instanceof Ci.nsIURI, "parsed is a nsIURI"); + is(parsed.prePath, "https://www.example.com", "prePath is correct"); + is(parsed.pathQueryRef, "/", "pathQueryRef is corect"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters("https://www.example.com/foobar", schema)[0], "Origin cannot contain a path part"); + ok(!PoliciesValidator.validateAndParseParameters("https://:!$%", schema)[0], "Invalid origin"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Invalid value"); +}); + +add_task(async function test_array_values() { + // The types inside an array object must all be the same + let schema = { + type: "array", + items: { + type: "number" + } + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters([1, 2, 3], schema); + ok(valid, "Array is valid"); + ok(Array.isArray(parsed), "parsed is an array"); + is(parsed.length, 3, "array is correct"); + + // An empty array is also valid + [valid, parsed] = PoliciesValidator.validateAndParseParameters([], schema); + ok(valid, "Array is valid"); + ok(Array.isArray(parsed), "parsed is an array"); + is(parsed.length, 0, "array is correct"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters([1, true, 3], schema)[0], "Mixed types"); + ok(!PoliciesValidator.validateAndParseParameters(2, schema)[0], "Type is correct but not in an array"); + ok(!PoliciesValidator.validateAndParseParameters({}, schema)[0], "Object is not an array"); +}); + +add_task(async function test_object_values() { + let schema = { + type: "object", + properties: { + url: { + type: "URL" + }, + title: { + type: "string" + } + } + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters( + { + url: "https://www.example.com/foo#bar", + title: "Foo", + alias: "Bar" + }, + schema); + + ok(valid, "Object is valid"); + ok(typeof(parsed) == "object", "parsed in an object"); + ok(parsed.url instanceof Ci.nsIURI, "types inside the object are also parsed"); + is(parsed.url.spec, "https://www.example.com/foo#bar", "URL was correctly parsed"); + is(parsed.title, "Foo", "title was correctly parsed"); + is(parsed.alias, undefined, "property not described in the schema is not present in the parsed object"); + + // Invalid values: + ok(!PoliciesValidator.validateAndParseParameters( + { + url: "https://www.example.com/foo#bar", + title: 3, + }, + schema)[0], "Mismatched type for title"); + + ok(!PoliciesValidator.validateAndParseParameters( + { + url: "www.example.com", + title: 3, + }, + schema)[0], "Invalid URL inside the object"); +}); + +add_task(async function test_array_of_objects() { + // This schema is used, for example, for bookmarks + let schema = { + type: "array", + items: { + type: "object", + properties: { + url: { + type: "URL", + }, + title: { + type: "string" + } + } + } + }; + + let valid, parsed; + [valid, parsed] = PoliciesValidator.validateAndParseParameters( + [{ + url: "https://www.example.com/bookmark1", + title: "Foo", + }, + { + url: "https://www.example.com/bookmark2", + title: "Bar", + }], + schema); + + ok(valid, "Array is valid"); + is(parsed.length, 2, "Correct number of items"); + + ok(typeof(parsed[0]) == "object" && typeof(parsed[1]) == "object", "Correct objects inside array"); + + is(parsed[0].url.spec, "https://www.example.com/bookmark1", "Correct URL for bookmark 1"); + is(parsed[1].url.spec, "https://www.example.com/bookmark2", "Correct URL for bookmark 2"); + + is(parsed[0].title, "Foo", "Correct title for bookmark 1"); + is(parsed[1].title, "Bar", "Correct title for bookmark 2"); +}); diff --git a/browser/components/enterprisepolicies/tests/browser/config_broken_json.json b/browser/components/enterprisepolicies/tests/browser/config_broken_json.json new file mode 100644 index 000000000000..7e13efdd8880 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/config_broken_json.json @@ -0,0 +1,3 @@ +{ + "policies +} diff --git a/browser/components/enterprisepolicies/tests/browser/config_simple_policies.json b/browser/components/enterprisepolicies/tests/browser/config_simple_policies.json new file mode 100644 index 000000000000..0fc759605f79 --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/config_simple_policies.json @@ -0,0 +1,8 @@ +{ + "policies": { + "simple_policy0": true, + "simple_policy1": true, + "simple_policy2": true, + "simple_policy3": false + } +} diff --git a/browser/components/enterprisepolicies/tests/browser/head.js b/browser/components/enterprisepolicies/tests/browser/head.js new file mode 100644 index 000000000000..9bbfc485eeda --- /dev/null +++ b/browser/components/enterprisepolicies/tests/browser/head.js @@ -0,0 +1,34 @@ +/* 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"; + +async function setupPolicyEngineWithJson(jsonName, customSchema) { + let filePath = getTestFilePath(jsonName ? jsonName : "non-existing-file.json"); + Services.prefs.setStringPref("browser.policies.alternatePath", filePath); + + let resolve = null; + let promise = new Promise((r) => resolve = r); + + 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 = Cu.import("resource:///modules/policies/schema.jsm", {}); + schemaModule.schema = customSchema; + } + + Services.obs.notifyObservers(null, "EnterprisePolicies:Restart"); + return promise; +} + +async function startWithCleanSlate() { + await setupPolicyEngineWithJson(""); + is(Services.policies.status, Ci.nsIEnterprisePolicies.INACTIVE, "Engine is inactive"); +} diff --git a/browser/components/enterprisepolicies/tests/moz.build b/browser/components/enterprisepolicies/tests/moz.build new file mode 100644 index 000000000000..9773431fcb3a --- /dev/null +++ b/browser/components/enterprisepolicies/tests/moz.build @@ -0,0 +1,12 @@ +# -*- 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", "General") + +BROWSER_CHROME_MANIFESTS += [ + 'browser/browser.ini' +] diff --git a/browser/components/moz.build b/browser/components/moz.build index f92c4b3439e7..aed15a4c98f4 100644 --- a/browser/components/moz.build +++ b/browser/components/moz.build @@ -38,6 +38,7 @@ DIRS += [ 'customizableui', 'dirprovider', 'downloads', + 'enterprisepolicies', 'extensions', 'feeds', 'migration', diff --git a/browser/installer/package-manifest.in b/browser/installer/package-manifest.in index b215cada67ce..d88d22784e4a 100644 --- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -229,6 +229,7 @@ @RESPATH@/components/dom_presentation.xpt @RESPATH@/components/downloads.xpt @RESPATH@/components/editor.xpt +@RESPATH@/components/enterprisepolicies.xpt @RESPATH@/components/extensions.xpt @RESPATH@/components/exthandler.xpt @RESPATH@/components/fastfind.xpt @@ -379,6 +380,9 @@ @RESPATH@/browser/components/browser-newtab.xpt @RESPATH@/browser/components/aboutNewTabService.js @RESPATH@/browser/components/NewTabComponents.manifest +@RESPATH@/browser/components/EnterprisePolicies.js +@RESPATH@/browser/components/EnterprisePoliciesContent.js +@RESPATH@/browser/components/EnterprisePolicies.manifest @RESPATH@/components/Downloads.manifest @RESPATH@/components/DownloadLegacy.js @RESPATH@/components/thumbnails.xpt diff --git a/toolkit/components/enterprisepolicies/moz.build b/toolkit/components/enterprisepolicies/moz.build new file mode 100644 index 000000000000..87b64c8b046e --- /dev/null +++ b/toolkit/components/enterprisepolicies/moz.build @@ -0,0 +1,14 @@ +# -*- 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' diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl new file mode 100644 index 000000000000..58252c7bd5da --- /dev/null +++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl @@ -0,0 +1,18 @@ +/* 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" + +[scriptable, uuid(6a568972-cc91-4bf5-963e-3768f3319b8a)] +interface nsIEnterprisePolicies : nsISupports +{ + const unsigned short UNINITIALIZED = 0; + const unsigned short INACTIVE = 1; + const unsigned short ACTIVE = 2; + const unsigned short FAILED = 3; + + readonly attribute short status; + + bool isAllowed(in ACString feature); +}; diff --git a/toolkit/components/moz.build b/toolkit/components/moz.build index 916c2b8dbcbd..3a800b463c2f 100644 --- a/toolkit/components/moz.build +++ b/toolkit/components/moz.build @@ -30,6 +30,7 @@ DIRS += [ 'crashmonitor', 'diskspacewatcher', 'downloads', + 'enterprisepolicies', 'extensions', 'filewatcher', 'finalizationwitness', diff --git a/toolkit/modules/Services.jsm b/toolkit/modules/Services.jsm index 23066384252d..60b53727a26f 100644 --- a/toolkit/modules/Services.jsm +++ b/toolkit/modules/Services.jsm @@ -122,6 +122,9 @@ if (AppConstants.MOZ_GECKO_PROFILER) { if (AppConstants.MOZ_TOOLKIT_SEARCH) { initTable.search = ["@mozilla.org/browser/search-service;1", "nsIBrowserSearchService"]; } +if (AppConstants.MOZ_BUILD_APP == "browser") { + initTable.policies = ["@mozilla.org/browser/enterprisepolicies;1", "nsIEnterprisePolicies"]; +} XPCOMUtils.defineLazyServiceGetters(Services, initTable); diff --git a/toolkit/modules/tests/xpcshell/test_Services.js b/toolkit/modules/tests/xpcshell/test_Services.js index 9601b81095e9..dc41f1ff9798 100644 --- a/toolkit/modules/tests/xpcshell/test_Services.js +++ b/toolkit/modules/tests/xpcshell/test_Services.js @@ -70,6 +70,10 @@ function run_test() { if ("nsIAndroidBridge" in Ci) { checkService("androidBridge", Ci.nsIAndroidBridge); } + if ("@mozilla.org/browser/enterprisepolicies;1" in Cc) { + checkService("policies", Ci.nsIEnterprisePolicies); + } + // In xpcshell tests, the "@mozilla.org/xre/app-info;1" component implements // only the nsIXULRuntime interface, but not nsIXULAppInfo. To test the diff --git a/toolkit/xre/nsXREDirProvider.cpp b/toolkit/xre/nsXREDirProvider.cpp index 84a3b78efecb..5705bd06d53e 100644 --- a/toolkit/xre/nsXREDirProvider.cpp +++ b/toolkit/xre/nsXREDirProvider.cpp @@ -1009,6 +1009,12 @@ nsXREDirProvider::DoStartup() static const char16_t kStartup[] = {'s','t','a','r','t','u','p','\0'}; obsSvc->NotifyObservers(nullptr, "profile-do-change", kStartup); + // Initialize the Enterprise Policies service + nsCOMPtr policies(do_GetService("@mozilla.org/browser/enterprisepolicies;1")); + if (policies) { + policies->Observe(nullptr, "policies-startup", nullptr); + } + // Init the Extension Manager nsCOMPtr em = do_GetService("@mozilla.org/addons/integration;1"); if (em) {