Bug 1419102 - Basis for the enterprise policy engine. r=Mossop draft
authorFelipe Gomes <felipc@gmail.com>
Fri, 19 Jan 2018 11:18:56 -0200
changeset 722684 afa409b9ef5a407dd26e47a2a9182fb3fc4ab1ad
parent 722672 6ffbba9ce0ef9ec77a63445f068f2e218ed4830f
child 722685 28caf609b51c46bff19a5bc143cb9c01ee5d60b7
push id96199
push userfelipc@gmail.com
push dateFri, 19 Jan 2018 15:05:44 +0000
reviewersMossop
bugs1419102
milestone59.0a1
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
.eslintignore
browser/components/enterprisepolicies/EnterprisePolicies.js
browser/components/enterprisepolicies/EnterprisePolicies.manifest
browser/components/enterprisepolicies/Policies.jsm
browser/components/enterprisepolicies/helpers/moz.build
browser/components/enterprisepolicies/helpers/sample.json
browser/components/enterprisepolicies/moz.build
browser/components/enterprisepolicies/nsIEnterprisePolicies.idl
browser/components/enterprisepolicies/schemas/configuration.json
browser/components/enterprisepolicies/schemas/moz.build
browser/components/enterprisepolicies/schemas/policies.json
browser/components/enterprisepolicies/schemas/schema.jsm
browser/components/moz.build
browser/installer/package-manifest.in
toolkit/modules/Services.jsm
toolkit/modules/tests/xpcshell/test_Services.js
--- 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();