1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5var EXPORTED_SYMBOLS = ["EnterprisePoliciesManager"];
6
7const { XPCOMUtils } = ChromeUtils.import(
8  "resource://gre/modules/XPCOMUtils.jsm"
9);
10const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11const { AppConstants } = ChromeUtils.import(
12  "resource://gre/modules/AppConstants.jsm"
13);
14
15XPCOMUtils.defineLazyModuleGetters(this, {
16  WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.jsm",
17  macOSPoliciesParser:
18    "resource://gre/modules/policies/macOSPoliciesParser.jsm",
19  Policies: "resource:///modules/policies/Policies.jsm",
20  JsonSchemaValidator:
21    "resource://gre/modules/components-utils/JsonSchemaValidator.jsm",
22});
23
24// This is the file that will be searched for in the
25// ${InstallDir}/distribution folder.
26const POLICIES_FILENAME = "policies.json";
27
28// When true browser policy is loaded per-user from
29// /run/user/$UID/appname
30const PREF_PER_USER_DIR = "toolkit.policies.perUserDir";
31// For easy testing, modify the helpers/sample.json file,
32// and set PREF_ALTERNATE_PATH in firefox.js as:
33// /your/repo/browser/components/enterprisepolicies/helpers/sample.json
34const PREF_ALTERNATE_PATH = "browser.policies.alternatePath";
35// For testing GPO, you can set an alternate location in testing
36const PREF_ALTERNATE_GPO = "browser.policies.alternateGPO";
37
38// For testing, we may want to set PREF_ALTERNATE_PATH to point to a file
39// relative to the test root directory. In order to enable this, the string
40// below may be placed at the beginning of that preference value and it will
41// be replaced with the path to the test root directory.
42const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
43const PREF_TEST_ROOT = "mochitest.testRoot";
44
45const PREF_LOGLEVEL = "browser.policies.loglevel";
46
47// To force disallowing enterprise-only policies during tests
48const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";
49
50// To allow for cleaning up old policies
51const PREF_POLICIES_APPLIED = "browser.policies.applied";
52
53XPCOMUtils.defineLazyGetter(this, "log", () => {
54  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
55  return new ConsoleAPI({
56    prefix: "Enterprise Policies",
57    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
58    // messages during development. See LOG_LEVELS in Console.jsm for details.
59    maxLogLevel: "error",
60    maxLogLevelPref: PREF_LOGLEVEL,
61  });
62});
63
64let env = Cc["@mozilla.org/process/environment;1"].getService(
65  Ci.nsIEnvironment
66);
67const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR");
68
69// We're only testing for empty objects, not
70// empty strings or empty arrays.
71function isEmptyObject(obj) {
72  if (typeof obj != "object" || Array.isArray(obj)) {
73    return false;
74  }
75  for (let key of Object.keys(obj)) {
76    if (!isEmptyObject(obj[key])) {
77      return false;
78    }
79  }
80  return true;
81}
82
83function EnterprisePoliciesManager() {
84  Services.obs.addObserver(this, "profile-after-change", true);
85  Services.obs.addObserver(this, "final-ui-startup", true);
86  Services.obs.addObserver(this, "sessionstore-windows-restored", true);
87  Services.obs.addObserver(this, "EnterprisePolicies:Restart", true);
88}
89
90EnterprisePoliciesManager.prototype = {
91  QueryInterface: ChromeUtils.generateQI([
92    "nsIObserver",
93    "nsISupportsWeakReference",
94    "nsIEnterprisePolicies",
95  ]),
96
97  _initialize() {
98    if (Services.prefs.getBoolPref(PREF_POLICIES_APPLIED, false)) {
99      if ("_cleanup" in Policies) {
100        let policyImpl = Policies._cleanup;
101
102        for (let timing of Object.keys(this._callbacks)) {
103          let policyCallback = policyImpl[timing];
104          if (policyCallback) {
105            this._schedulePolicyCallback(
106              timing,
107              policyCallback.bind(
108                policyImpl,
109                this /* the EnterprisePoliciesManager */
110              )
111            );
112          }
113        }
114      }
115      Services.prefs.clearUserPref(PREF_POLICIES_APPLIED);
116    }
117
118    let provider = this._chooseProvider();
119
120    if (provider.failed) {
121      this.status = Ci.nsIEnterprisePolicies.FAILED;
122      return;
123    }
124
125    if (!provider.hasPolicies) {
126      this.status = Ci.nsIEnterprisePolicies.INACTIVE;
127      return;
128    }
129
130    this.status = Ci.nsIEnterprisePolicies.ACTIVE;
131    this._parsedPolicies = {};
132    Services.telemetry.scalarSet(
133      "policies.count",
134      Object.keys(provider.policies).length
135    );
136    this._activatePolicies(provider.policies);
137
138    Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true);
139  },
140
141  _chooseProvider() {
142    let platformProvider = null;
143    if (AppConstants.platform == "win") {
144      platformProvider = new WindowsGPOPoliciesProvider();
145    } else if (AppConstants.platform == "macosx") {
146      platformProvider = new macOSPoliciesProvider();
147    }
148    let jsonProvider = new JSONPoliciesProvider();
149    if (platformProvider && platformProvider.hasPolicies) {
150      if (jsonProvider.hasPolicies) {
151        return new CombinedProvider(platformProvider, jsonProvider);
152      }
153      return platformProvider;
154    }
155    return jsonProvider;
156  },
157
158  _activatePolicies(unparsedPolicies) {
159    let { schema } = ChromeUtils.import(
160      "resource:///modules/policies/schema.jsm"
161    );
162
163    for (let policyName of Object.keys(unparsedPolicies)) {
164      let policySchema = schema.properties[policyName];
165      let policyParameters = unparsedPolicies[policyName];
166
167      if (!policySchema) {
168        log.error(`Unknown policy: ${policyName}`);
169        continue;
170      }
171
172      if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
173        log.error(`Policy ${policyName} is only allowed on ESR`);
174        continue;
175      }
176
177      let {
178        valid: parametersAreValid,
179        parsedValue: parsedParameters,
180      } = JsonSchemaValidator.validate(policyParameters, policySchema, {
181        allowExtraProperties: true,
182      });
183
184      if (!parametersAreValid) {
185        log.error(`Invalid parameters specified for ${policyName}.`);
186        continue;
187      }
188
189      this._parsedPolicies[policyName] = parsedParameters;
190      let policyImpl = Policies[policyName];
191
192      for (let timing of Object.keys(this._callbacks)) {
193        let policyCallback = policyImpl[timing];
194        if (policyCallback) {
195          this._schedulePolicyCallback(
196            timing,
197            policyCallback.bind(
198              policyImpl,
199              this /* the EnterprisePoliciesManager */,
200              parsedParameters
201            )
202          );
203        }
204      }
205    }
206  },
207
208  _callbacks: {
209    // The earliest that a policy callback can run. This will
210    // happen right after the Policy Engine itself has started,
211    // and before the Add-ons Manager has started.
212    onBeforeAddons: [],
213
214    // This happens after all the initialization related to
215    // the profile has finished (prefs, places database, etc.).
216    onProfileAfterChange: [],
217
218    // Just before the first browser window gets created.
219    onBeforeUIStartup: [],
220
221    // Called after all windows from the last session have been
222    // restored (or the default window and homepage tab, if the
223    // session is not being restored).
224    // The content of the tabs themselves have not necessarily
225    // finished loading.
226    onAllWindowsRestored: [],
227  },
228
229  _schedulePolicyCallback(timing, callback) {
230    this._callbacks[timing].push(callback);
231  },
232
233  _runPoliciesCallbacks(timing) {
234    let callbacks = this._callbacks[timing];
235    while (callbacks.length) {
236      let callback = callbacks.shift();
237      try {
238        callback();
239      } catch (ex) {
240        log.error("Error running ", callback, `for ${timing}:`, ex);
241      }
242    }
243  },
244
245  async _restart() {
246    DisallowedFeatures = {};
247
248    Services.ppmm.sharedData.delete("EnterprisePolicies:Status");
249    Services.ppmm.sharedData.delete("EnterprisePolicies:DisallowedFeatures");
250
251    this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED;
252    this._parsedPolicies = undefined;
253    for (let timing of Object.keys(this._callbacks)) {
254      this._callbacks[timing] = [];
255    }
256
257    let { PromiseUtils } = ChromeUtils.import(
258      "resource://gre/modules/PromiseUtils.jsm"
259    );
260    // Simulate the startup process. This step-by-step is a bit ugly but it
261    // tries to emulate the same behavior as of a normal startup.
262
263    await PromiseUtils.idleDispatch(() => {
264      this.observe(null, "policies-startup", null);
265    });
266
267    await PromiseUtils.idleDispatch(() => {
268      this.observe(null, "profile-after-change", null);
269    });
270
271    await PromiseUtils.idleDispatch(() => {
272      this.observe(null, "final-ui-startup", null);
273    });
274
275    await PromiseUtils.idleDispatch(() => {
276      this.observe(null, "sessionstore-windows-restored", null);
277    });
278  },
279
280  // nsIObserver implementation
281  observe: function BG_observe(subject, topic, data) {
282    switch (topic) {
283      case "policies-startup":
284        // Before the first set of policy callbacks runs, we must
285        // initialize the service.
286        this._initialize();
287
288        this._runPoliciesCallbacks("onBeforeAddons");
289        break;
290
291      case "profile-after-change":
292        this._runPoliciesCallbacks("onProfileAfterChange");
293        break;
294
295      case "final-ui-startup":
296        this._runPoliciesCallbacks("onBeforeUIStartup");
297        break;
298
299      case "sessionstore-windows-restored":
300        this._runPoliciesCallbacks("onAllWindowsRestored");
301
302        // After the last set of policy callbacks ran, notify the test observer.
303        Services.obs.notifyObservers(
304          null,
305          "EnterprisePolicies:AllPoliciesApplied"
306        );
307        break;
308
309      case "EnterprisePolicies:Restart":
310        this._restart().then(null, Cu.reportError);
311        break;
312    }
313  },
314
315  disallowFeature(feature, neededOnContentProcess = false) {
316    DisallowedFeatures[feature] = neededOnContentProcess;
317
318    // NOTE: For optimization purposes, only features marked as needed
319    // on content process will be passed onto the child processes.
320    if (neededOnContentProcess) {
321      Services.ppmm.sharedData.set(
322        "EnterprisePolicies:DisallowedFeatures",
323        new Set(
324          Object.keys(DisallowedFeatures).filter(key => DisallowedFeatures[key])
325        )
326      );
327    }
328  },
329
330  // ------------------------------
331  // public nsIEnterprisePolicies members
332  // ------------------------------
333
334  _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,
335
336  set status(val) {
337    this._status = val;
338    if (val != Ci.nsIEnterprisePolicies.INACTIVE) {
339      Services.ppmm.sharedData.set("EnterprisePolicies:Status", val);
340    }
341  },
342
343  get status() {
344    return this._status;
345  },
346
347  isAllowed: function BG_sanitize(feature) {
348    return !(feature in DisallowedFeatures);
349  },
350
351  getActivePolicies() {
352    return this._parsedPolicies;
353  },
354
355  setSupportMenu(supportMenu) {
356    SupportMenu = supportMenu;
357  },
358
359  getSupportMenu() {
360    return SupportMenu;
361  },
362
363  setExtensionPolicies(extensionPolicies) {
364    ExtensionPolicies = extensionPolicies;
365  },
366
367  getExtensionPolicy(extensionID) {
368    if (ExtensionPolicies && extensionID in ExtensionPolicies) {
369      return ExtensionPolicies[extensionID];
370    }
371    return null;
372  },
373
374  setExtensionSettings(extensionSettings) {
375    ExtensionSettings = extensionSettings;
376    if (
377      "*" in extensionSettings &&
378      "install_sources" in extensionSettings["*"]
379    ) {
380      InstallSources = new MatchPatternSet(
381        extensionSettings["*"].install_sources
382      );
383    }
384  },
385
386  getExtensionSettings(extensionID) {
387    let settings = null;
388    if (ExtensionSettings) {
389      if (extensionID in ExtensionSettings) {
390        settings = ExtensionSettings[extensionID];
391      } else if ("*" in ExtensionSettings) {
392        settings = ExtensionSettings["*"];
393      }
394    }
395    return settings;
396  },
397
398  mayInstallAddon(addon) {
399    // See https://dev.chromium.org/administrators/policy-list-3/extension-settings-full
400    if (!ExtensionSettings) {
401      return true;
402    }
403    if (addon.id in ExtensionSettings) {
404      if ("installation_mode" in ExtensionSettings[addon.id]) {
405        switch (ExtensionSettings[addon.id].installation_mode) {
406          case "blocked":
407            return false;
408          default:
409            return true;
410        }
411      }
412    }
413    if ("*" in ExtensionSettings) {
414      if (
415        ExtensionSettings["*"].installation_mode &&
416        ExtensionSettings["*"].installation_mode == "blocked"
417      ) {
418        return false;
419      }
420      if ("allowed_types" in ExtensionSettings["*"]) {
421        return ExtensionSettings["*"].allowed_types.includes(addon.type);
422      }
423    }
424    return true;
425  },
426
427  allowedInstallSource(uri) {
428    return InstallSources ? InstallSources.matches(uri) : true;
429  },
430};
431
432let DisallowedFeatures = {};
433let SupportMenu = null;
434let ExtensionPolicies = null;
435let ExtensionSettings = null;
436let InstallSources = null;
437
438/**
439 * areEnterpriseOnlyPoliciesAllowed
440 *
441 * Checks whether the policies marked as enterprise_only in the
442 * schema are allowed to run on this browser.
443 *
444 * This is meant to only allow policies to run on ESR, but in practice
445 * we allow it to run on channels different than release, to allow
446 * these policies to be tested on pre-release channels.
447 *
448 * @returns {Bool} Whether the policy can run.
449 */
450function areEnterpriseOnlyPoliciesAllowed() {
451  if (Cu.isInAutomation || isXpcshell) {
452    if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
453      // This is used as an override to test the "enterprise_only"
454      // functionality itself on tests.
455      return false;
456    }
457    return true;
458  }
459
460  return (
461    AppConstants.IS_ESR ||
462    AppConstants.MOZ_DEV_EDITION ||
463    AppConstants.NIGHTLY_BUILD
464  );
465}
466
467/*
468 * JSON PROVIDER OF POLICIES
469 *
470 * This is a platform-agnostic provider which looks for
471 * policies specified through a policies.json file stored
472 * in the installation's distribution folder.
473 */
474
475class JSONPoliciesProvider {
476  constructor() {
477    this._policies = null;
478    this._readData();
479  }
480
481  get hasPolicies() {
482    return this._policies !== null && !isEmptyObject(this._policies);
483  }
484
485  get policies() {
486    return this._policies;
487  }
488
489  get failed() {
490    return this._failed;
491  }
492
493  _getConfigurationFile() {
494    let configFile = null;
495
496    if (AppConstants.platform == "linux") {
497      let systemConfigFile = Cc["@mozilla.org/file/local;1"].createInstance(
498        Ci.nsIFile
499      );
500      systemConfigFile.initWithPath(
501        "/etc/" + Services.appinfo.name.toLowerCase() + "/policies"
502      );
503      systemConfigFile.append(POLICIES_FILENAME);
504      if (systemConfigFile.exists()) {
505        return systemConfigFile;
506      }
507    }
508
509    try {
510      let perUserPath = Services.prefs.getBoolPref(PREF_PER_USER_DIR, false);
511      if (perUserPath) {
512        configFile = Services.dirsvc.get("XREUserRunTimeDir", Ci.nsIFile);
513      } else {
514        configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
515      }
516      configFile.append(POLICIES_FILENAME);
517    } catch (ex) {
518      // Getting the correct directory will fail in xpcshell tests. This should
519      // be handled the same way as if the configFile simply does not exist.
520    }
521
522    let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH, "");
523
524    // Check if we are in automation *before* we use the synchronous
525    // nsIFile.exists() function or allow the config file to be overriden
526    // An alternate policy path can also be used in Nightly builds (for
527    // testing purposes), but the Background Update Agent will be unable to
528    // detect the alternate policy file so the DisableAppUpdate policy may not
529    // work as expected.
530    if (
531      alternatePath &&
532      (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) &&
533      (!configFile || !configFile.exists())
534    ) {
535      if (alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
536        // Intentionally not using a default value on this pref lookup. If no
537        // test root is set, we are not currently testing and this function
538        // should throw rather than returning something.
539        let testRoot = Services.prefs.getStringPref(PREF_TEST_ROOT);
540        let relativePath = alternatePath.substring(
541          MAGIC_TEST_ROOT_PREFIX.length
542        );
543        if (AppConstants.platform == "win") {
544          relativePath = relativePath.replace(/\//g, "\\");
545        }
546        alternatePath = testRoot + relativePath;
547      }
548
549      configFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
550      configFile.initWithPath(alternatePath);
551    }
552
553    return configFile;
554  }
555
556  _readData() {
557    let configFile = this._getConfigurationFile();
558    if (!configFile) {
559      // Do nothing, _policies will remain null
560      return;
561    }
562    try {
563      let data = Cu.readUTF8File(configFile);
564      if (data) {
565        log.debug(`policies.json path = ${configFile.path}`);
566        log.debug(`policies.json content = ${data}`);
567        this._policies = JSON.parse(data).policies;
568
569        if (!this._policies) {
570          log.error("Policies file doesn't contain a 'policies' object");
571          this._failed = true;
572        }
573      }
574    } catch (ex) {
575      if (
576        ex instanceof Components.Exception &&
577        ex.result == Cr.NS_ERROR_FILE_NOT_FOUND
578      ) {
579        // Do nothing, _policies will remain null
580      } else if (ex instanceof SyntaxError) {
581        log.error(`Error parsing JSON file: ${ex}`);
582        this._failed = true;
583      } else {
584        log.error(`Error reading JSON file: ${ex}`);
585        this._failed = true;
586      }
587    }
588  }
589}
590
591class WindowsGPOPoliciesProvider {
592  constructor() {
593    this._policies = null;
594
595    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
596      Ci.nsIWindowsRegKey
597    );
598
599    // Machine policies override user policies, so we read
600    // user policies first and then replace them if necessary.
601    this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
602    // We don't access machine policies in testing
603    if (!Cu.isInAutomation && !isXpcshell) {
604      this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
605    }
606  }
607
608  get hasPolicies() {
609    return this._policies !== null && !isEmptyObject(this._policies);
610  }
611
612  get policies() {
613    return this._policies;
614  }
615
616  get failed() {
617    return this._failed;
618  }
619
620  _readData(wrk, root) {
621    try {
622      let regLocation = "SOFTWARE\\Policies";
623      if (Cu.isInAutomation || isXpcshell) {
624        try {
625          regLocation = Services.prefs.getStringPref(PREF_ALTERNATE_GPO);
626        } catch (e) {}
627      }
628      wrk.open(root, regLocation, wrk.ACCESS_READ);
629      if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
630        log.debug(
631          `root = ${
632            root == wrk.ROOT_KEY_CURRENT_USER
633              ? "HKEY_CURRENT_USER"
634              : "HKEY_LOCAL_MACHINE"
635          }`
636        );
637        this._policies = WindowsGPOParser.readPolicies(wrk, this._policies);
638      }
639      wrk.close();
640    } catch (e) {
641      log.error("Unable to access registry - ", e);
642    }
643  }
644}
645
646class macOSPoliciesProvider {
647  constructor() {
648    this._policies = null;
649    let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance(
650      Ci.nsIMacPreferencesReader
651    );
652    if (!prefReader.policiesEnabled()) {
653      return;
654    }
655    this._policies = macOSPoliciesParser.readPolicies(prefReader);
656  }
657
658  get hasPolicies() {
659    return this._policies !== null && Object.keys(this._policies).length;
660  }
661
662  get policies() {
663    return this._policies;
664  }
665
666  get failed() {
667    return this._failed;
668  }
669}
670
671class CombinedProvider {
672  constructor(primaryProvider, secondaryProvider) {
673    // Combine policies with primaryProvider taking precedence.
674    // We only do this for top level policies.
675    this._policies = primaryProvider._policies;
676    for (let policyName of Object.keys(secondaryProvider.policies)) {
677      if (!(policyName in this._policies)) {
678        this._policies[policyName] = secondaryProvider.policies[policyName];
679      }
680    }
681  }
682
683  get hasPolicies() {
684    // Combined provider always has policies.
685    return true;
686  }
687
688  get policies() {
689    return this._policies;
690  }
691
692  get failed() {
693    // Combined provider never fails.
694    return false;
695  }
696}
697