Bug 1419102 - Basis for the enterprise policy engine. r=Mossop
* * *
[mq]: updatepatch
MozReview-Commit-ID: JDcrcFJlyV6
* * *
try: -b o -p linux,linux64,macosx64,win32,win64 -u none -t none
--- a/.eslintignore
+++ b/.eslintignore
@@ -72,16 +72,20 @@ browser/branding/**/firefox-branding.js
# Gzipped test file.
browser/base/content/test/general/gZipOfflineChild.html
browser/base/content/test/urlbar/file_blank_but_not_blank.html
# New tab is likely to be replaced soon.
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
# their own lint rules currently.
browser/extensions/followonsearch/**
browser/extensions/screenshots/**
browser/extensions/pdfjs/content/build**
browser/extensions/pdfjs/content/web**
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.js
@@ -0,0 +1,286 @@
+/* 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");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ Policies: "resource:///modules/policies/Policies.jsm",
+ PoliciesValidator: "resource:///modules/policies/Policies.jsm",
+});
+
+function LOG(s) {
+ Services.console.logStringMessage("$ POLICIES $: " + s + "\n");
+ dump("% POLICIES %: " + s + "\n");
+}
+
+// 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";
+
+// File mode constants
+const MODE_RDONLY = 0x01;
+const PERMS_FILE = 0o644;
+
+
+
+// ==== 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() {
+ LOG("STARTING CONSTRUCTOR");
+
+ Services.obs.addObserver(this, "profile-after-change", true);
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", 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("This policy is not defined in the schema.");
+ continue;
+ }
+
+ if (!PoliciesValidator.validateAndParseParameters(policyName,
+ policyParameters,
+ policySchema)) {
+ LOG(`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 */
+ policyParameters));
+ }
+ }
+ }
+ },
+
+ _callbacks: {
+ 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) {}
+ }
+ },
+
+ // nsIObserver implementation
+ observe: function BG_observe(subject, topic, data) {
+ switch (topic) {
+ case "profile-after-change":
+ this._initialize();
+ this._runPoliciesCallbacks("ProfileAfterChange");
+ break;
+
+ case "final-ui-startup":
+ this._runPoliciesCallbacks("BeforeUIStartup");
+ break;
+
+ case "sessionstore-windows-restored":
+ this._runPoliciesCallbacks("AllWindowsRestored");
+ break;
+ }
+ },
+
+ disallowFeature(feature) {
+ DisallowedFeatures[feature] = true;
+ },
+
+ // ------------------------------
+ // public nsIEnterprisePolicies members
+ // ------------------------------
+
+ _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,
+
+ set status(val) {
+ this._status = val;
+ 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() {
+ let inputStream;
+ try {
+ inputStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+
+ inputStream.init(this._file, MODE_RDONLY, PERMS_FILE, 0);
+ this._data.exists = true;
+
+ let bytes = NetUtil.readInputStream(inputStream, inputStream.available());
+ this._data.json = JSON.parse(new TextDecoder().decode(bytes));
+ } 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 parsing JSON file");
+ this._data.failed = true;
+ } else {
+ LOG("Error reading file");
+ this._data.failed = true;
+ }
+ } finally {
+ inputStream.close();
+ }
+ }
+};
+
+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);
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/EnterprisePolicies.manifest
@@ -0,0 +1,3 @@
+component {ea4e1414-779b-458b-9d1f-d18e8efbc145} EnterprisePolicies.js process=main
+contract @mozilla.org/browser/enterprisepolicies;1 {ea4e1414-779b-458b-9d1f-d18e8efbc145} process=main
+category app-startup EnterprisePoliciesManager service,@mozilla.org/browser/enterprisepolicies;1 process=main
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/Policies.jsm
@@ -0,0 +1,41 @@
+/* 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");
+
+this.EXPORTED_SYMBOLS = ["Policies", "PoliciesValidator"];
+
+this.PoliciesValidator = {
+ validateAndParseParameters() {
+ return true;
+ }
+}
+
+this.Policies = {
+ "block_about_config": {
+ onProfileAfterChange(manager, param) {
+ if (param == true) {
+ manager.disallowFeature("about:config");
+ }
+ }
+ },
+
+ "block_devtools": {
+ onProfileAfterChange(manager, param) {
+ if (param == true) {
+ manager.disallowFeature("devtools");
+ }
+ }
+ },
+
+ "bookmarks_on_menu": {}
+};
new file mode 100644
--- /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")
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/helpers/sample.json
@@ -0,0 +1,11 @@
+{
+ "policies": {
+ "block_about_config": true,
+ "block_devtools": true,
+ "bookmarks_on_menu": [
+ "https://www.mozilla.org/firefox/new/",
+ "https://www.example.com",
+ "https://www.example.org"
+ ]
+ }
+}
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/moz.build
@@ -0,0 +1,34 @@
+# -*- 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',
+]
+
+XPIDL_SOURCES += [
+ 'nsIEnterprisePolicies.idl',
+]
+
+XPIDL_MODULE = 'enterprisepolicies'
+
+EXTRA_COMPONENTS += [
+ 'EnterprisePolicies.js',
+ 'EnterprisePolicies.manifest',
+]
+
+EXTRA_JS_MODULES.policies += [
+ 'Policies.jsm',
+]
+
+#BROWSER_CHROME_MANIFESTS += [
+# 'tests/browser/browser.ini'
+#]
+
+FINAL_LIBRARY = 'browsercomps'
new file mode 100644
--- /dev/null
+++ b/browser/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);
+};
new file mode 100644
--- /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"]
+}
\ No newline at end of file
new file mode 100644
--- /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',
+]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/schemas/policies.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "type": "object",
+ "properties": {
+ "block_about_config": {
+ "description": "Blocks access to the about:config page.",
+ "first_available": "59.0",
+
+ "type": "boolean",
+ "enum": [true]
+ },
+
+ "block_devtools": {
+ "description": "Blocks access to the developer tools.",
+ "first_available": "59.0",
+
+ "type": "boolean",
+ "enum": [true]
+ },
+
+ "bookmarks_on_menu": {
+ "description": "Adds a set of bookmarks to the Bookmarks Menu.",
+ "first_available": "59.0",
+ "run_on_modified": true,
+
+ "type": "array",
+ "items": {
+ "type": "URL"
+ }
+ }
+ }
+}
new file mode 100644
--- /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.json
--- a/browser/components/moz.build
+++ b/browser/components/moz.build
@@ -33,16 +33,17 @@ with Files('controlcenter/**'):
DIRS += [
'about',
'contextualidentity',
'customizableui',
'dirprovider',
'downloads',
+ 'enterprisepolicies',
'extensions',
'feeds',
'migration',
'newtab',
'originattributes',
'places',
'preferences',
'privatebrowsing',
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -170,16 +170,17 @@
#endif
@RESPATH@/components/appshell.xpt
@RESPATH@/components/appstartup.xpt
@RESPATH@/components/autocomplete.xpt
@RESPATH@/components/autoconfig.xpt
@RESPATH@/components/browser-element.xpt
@RESPATH@/browser/components/browsercompsbase.xpt
@RESPATH@/browser/components/browser-feeds.xpt
+@RESPATH@/browser/components/enterprisepolicies.xpt
@RESPATH@/components/caps.xpt
@RESPATH@/components/chrome.xpt
#ifdef MOZ_CODE_COVERAGE
@RESPATH@/components/code-coverage.xpt
#endif
@RESPATH@/components/commandhandler.xpt
@RESPATH@/components/commandlines.xpt
@RESPATH@/components/composer.xpt
@@ -374,16 +375,19 @@
@RESPATH@/browser/components/aboutdevtools.manifest
@RESPATH@/browser/components/aboutdevtoolstoolbox-registration.js
@RESPATH@/browser/components/aboutdevtoolstoolbox.manifest
@RESPATH@/browser/components/Experiments.manifest
@RESPATH@/browser/components/ExperimentsService.js
@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
@RESPATH@/components/PageThumbsComponents.manifest
@RESPATH@/components/crashmonitor.manifest
@RESPATH@/components/nsCrashMonitor.js
@RESPATH@/components/toolkitsearch.manifest
@RESPATH@/components/nsSearchService.js
--- a/toolkit/modules/Services.jsm
+++ b/toolkit/modules/Services.jsm
@@ -117,12 +117,15 @@ if (AppConstants.platform == "android")
initTable.androidBridge = ["@mozilla.org/android/bridge;1", "nsIAndroidBridge"];
}
if (AppConstants.MOZ_GECKO_PROFILER) {
initTable.profiler = ["@mozilla.org/tools/profiler;1", "nsIProfiler"];
}
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);
initTable = undefined;
--- a/toolkit/modules/tests/xpcshell/test_Services.js
+++ b/toolkit/modules/tests/xpcshell/test_Services.js
@@ -65,16 +65,20 @@ function run_test() {
checkService("wm", Ci.nsIWindowMediator);
checkService("ww", Ci.nsIWindowWatcher);
if ("nsIBrowserSearchService" in Ci) {
checkService("search", Ci.nsIBrowserSearchService);
}
if ("nsIAndroidBridge" in Ci) {
checkService("androidBridge", Ci.nsIAndroidBridge);
}
+ if ("nsIEnterprisePolicies" in Ci) {
+ 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
// service getter for the latter interface, load mock app-info.
let tmp = {};
Cu.import("resource://testing-common/AppInfo.jsm", tmp);
tmp.updateAppInfo();