Bug 1498032 - Support flat policies on Mac. r=spohl
authorFelipe Gomes <felipc@gmail.com>
Thu, 11 Oct 2018 21:36:56 +0000
changeset 499233 c58e52e69803f3892fd1812694b6da1918906c35
parent 499232 31eac8d65771c51c5e1a099e1a57dadcc98fc57a
child 499234 facb6f18068e502530ea84492166826d3c88d4f5
push id1864
push userffxbld-merge
push dateMon, 03 Dec 2018 15:51:40 +0000
treeherdermozilla-release@f040763d99ad [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersspohl
bugs1498032
milestone64.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1498032 - Support flat policies on Mac. r=spohl Differential Revision: https://phabricator.services.mozilla.com/D8302
browser/components/enterprisepolicies/macOSPoliciesParser.jsm
browser/components/enterprisepolicies/tests/browser/browser.ini
browser/components/enterprisepolicies/tests/browser/browser_policies_macosparser_unflatten.js
browser/components/enterprisepolicies/tests/browser/browser_policies_sorted_alphabetically.js
--- a/browser/components/enterprisepolicies/macOSPoliciesParser.jsm
+++ b/browser/components/enterprisepolicies/macOSPoliciesParser.jsm
@@ -23,16 +23,17 @@ var EXPORTED_SYMBOLS = ["macOSPoliciesPa
 
 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));
     }
 
@@ -47,9 +48,107 @@ var macOSPoliciesParser = {
         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;
+}
--- a/browser/components/enterprisepolicies/tests/browser/browser.ini
+++ b/browser/components/enterprisepolicies/tests/browser/browser.ini
@@ -10,16 +10,18 @@ support-files =
   policy_websitefilter_exception.html
   ../../../../../toolkit/components/antitracking/test/browser/page.html
   ../../../../../toolkit/components/antitracking/test/browser/subResources.sjs
 
 [browser_policies_basic_tests.js]
 [browser_policies_broken_json.js]
 [browser_policies_enterprise_only.js]
 [browser_policies_getActivePolicies.js]
+[browser_policies_macosparser_unflatten.js]
+skip-if = os != 'mac'
 [browser_policies_notice_in_aboutpreferences.js]
 [browser_policies_popups_cookies_addons_flash.js]
 [browser_policies_runOnce_helper.js]
 [browser_policies_setAndLockPref_API.js]
 [browser_policies_simple_pref_policies.js]
 [browser_policies_sorted_alphabetically.js]
 [browser_policy_app_update.js]
 [browser_policy_app_update_URL.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_macosparser_unflatten.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+let { macOSPoliciesParser } = ChromeUtils.import("resource:///modules/policies/macOSPoliciesParser.jsm", {});
+
+add_task(async function test_object_unflatten() {
+  // Note: these policies are just examples and they won't actually
+  // run through the policy engine on this test. We're just testing
+  // that the unflattening algorithm produces the correct output.
+  let input = {
+    "DisplayBookmarksToolbar": true,
+
+    "Homepage__URL": "https://www.mozilla.org",
+    "Homepage__Locked": "true",
+    "Homepage__Additional__0": "https://extra-homepage-1.example.com",
+    "Homepage__Additional__1": "https://extra-homepage-2.example.com",
+
+    "WebsiteFilter__Block__0": "*://*.example.org/*",
+    "WebsiteFilter__Block__1": "*://*.example.net/*",
+    "WebsiteFilter__Exceptions__0": "*://*.example.org/*exception*",
+
+    "Permissions__Camera__Allow__0": "https://www.example.com",
+
+    "Permissions__Notifications__Allow__0": "https://www.example.com",
+    "Permissions__Notifications__Allow__1": "https://www.example.org",
+    "Permissions__Notifications__Block__0": "https://www.example.net",
+
+    "Permissions__Notifications__BlockNewRequests": true,
+    "Permissions__Notifications__Locked": true,
+
+    "Bookmarks__0__Title": "Bookmark 1",
+    "Bookmarks__0__URL": "https://bookmark1.example.com",
+
+    "Bookmarks__1__Title": "Bookmark 2",
+    "Bookmarks__1__URL": "https://bookmark2.example.com",
+    "Bookmarks__1__Folder": "Folder",
+  };
+
+  let expected = {
+    "DisplayBookmarksToolbar": true,
+
+    "Homepage": {
+      "URL": "https://www.mozilla.org",
+      "Locked": "true",
+      "Additional": [
+        "https://extra-homepage-1.example.com",
+        "https://extra-homepage-2.example.com",
+      ],
+    },
+
+    "WebsiteFilter": {
+      "Block": [
+        "*://*.example.org/*",
+        "*://*.example.net/*",
+      ],
+      "Exceptions": [
+        "*://*.example.org/*exception*",
+      ],
+    },
+
+    "Permissions": {
+      "Camera": {
+        "Allow": [
+          "https://www.example.com",
+        ],
+      },
+
+      "Notifications": {
+        "Allow": [
+          "https://www.example.com",
+          "https://www.example.org",
+        ],
+        "Block": [
+          "https://www.example.net",
+        ],
+        "BlockNewRequests": true,
+        "Locked": true,
+      },
+    },
+
+    "Bookmarks": [
+      {
+        "Title": "Bookmark 1",
+        "URL": "https://bookmark1.example.com",
+      },
+      {
+        "Title": "Bookmark 2",
+        "URL": "https://bookmark2.example.com",
+        "Folder": "Folder",
+      },
+    ],
+  };
+
+  let unflattened = macOSPoliciesParser.unflatten(input);
+
+  Assert.deepEqual(unflattened, expected, "Input was unflattened correctly.");
+});
+
+add_task(async function test_array_unflatten() {
+  let input = {
+    "Foo__1": 1,
+    "Foo__5": 5,
+    "Foo__10": 10,
+    "Foo__30": 30,
+    "Foo__51": 51, // This one should not be included as the limit is 50
+  };
+
+  let unflattened = macOSPoliciesParser.unflatten(input);
+  is(unflattened.Foo.length, 31, "Array size is correct");
+
+  let expected = {
+    Foo: [, 1, , , , 5], // eslint-disable-line no-sparse-arrays
+  };
+  expected.Foo[10] = 10;
+  expected.Foo[30] = 30;
+
+  Assert.deepEqual(unflattened, expected, "Array was unflattened correctly.");
+});
--- a/browser/components/enterprisepolicies/tests/browser/browser_policies_sorted_alphabetically.js
+++ b/browser/components/enterprisepolicies/tests/browser/browser_policies_sorted_alphabetically.js
@@ -18,8 +18,13 @@ function checkArrayIsSorted(array, msg) 
 
 add_task(async function test_policies_sorted() {
   let { schema } = ChromeUtils.import("resource:///modules/policies/schema.jsm", {});
   let { Policies } = ChromeUtils.import("resource:///modules/policies/Policies.jsm", {});
 
   checkArrayIsSorted(Object.keys(schema.properties), "policies-schema.json is alphabetically sorted.");
   checkArrayIsSorted(Object.keys(Policies), "Policies.jsm is alphabetically sorted.");
 });
+
+add_task(async function check_naming_conventions() {
+  let { schema } = ChromeUtils.import("resource:///modules/policies/schema.jsm", {});
+  is(Object.keys(schema.properties).some(key => key.includes("__")), false, "Can't use __ in a policy name as it's used as a delimiter");
+});