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    for (let timing of Object.keys(this._callbacks)) {
253      this._callbacks[timing] = [];
254    }
255
256    let { PromiseUtils } = ChromeUtils.import(
257      "resource://gre/modules/PromiseUtils.jsm"
258    );
259    // Simulate the startup process. This step-by-step is a bit ugly but it
260    // tries to emulate the same behavior as of a normal startup.
261
262    await PromiseUtils.idleDispatch(() => {
263      this.observe(null, "policies-startup", null);
264    });
265
266    await PromiseUtils.idleDispatch(() => {
267      this.observe(null, "profile-after-change", null);
268    });
269
270    await PromiseUtils.idleDispatch(() => {
271      this.observe(null, "final-ui-startup", null);
272    });
273
274    await PromiseUtils.idleDispatch(() => {
275      this.observe(null, "sessionstore-windows-restored", null);
276    });
277  },
278
279  // nsIObserver implementation
280  observe: function BG_observe(subject, topic, data) {
281    switch (topic) {
282      case "policies-startup":
283        // Before the first set of policy callbacks runs, we must
284        // initialize the service.
285        this._initialize();
286
287        this._runPoliciesCallbacks("onBeforeAddons");
288        break;
289
290      case "profile-after-change":
291        this._runPoliciesCallbacks("onProfileAfterChange");
292        break;
293
294      case "final-ui-startup":
295        this._runPoliciesCallbacks("onBeforeUIStartup");
296        break;
297
298      case "sessionstore-windows-restored":
299        this._runPoliciesCallbacks("onAllWindowsRestored");
300
301        // After the last set of policy callbacks ran, notify the test observer.
302        Services.obs.notifyObservers(
303          null,
304          "EnterprisePolicies:AllPoliciesApplied"
305        );
306        break;
307
308      case "EnterprisePolicies:Restart":
309        this._restart().then(null, Cu.reportError);
310        break;
311    }
312  },
313
314  disallowFeature(feature, neededOnContentProcess = false) {
315    DisallowedFeatures[feature] = neededOnContentProcess;
316
317    // NOTE: For optimization purposes, only features marked as needed
318    // on content process will be passed onto the child processes.
319    if (neededOnContentProcess) {
320      Services.ppmm.sharedData.set(
321        "EnterprisePolicies:DisallowedFeatures",
322        new Set(
323          Object.keys(DisallowedFeatures).filter(key => DisallowedFeatures[key])
324        )
325      );
326    }
327  },
328
329  // ------------------------------
330  // public nsIEnterprisePolicies members
331  // ------------------------------
332
333  _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,
334
335  set status(val) {
336    this._status = val;
337    if (val != Ci.nsIEnterprisePolicies.INACTIVE) {
338      Services.ppmm.sharedData.set("EnterprisePolicies:Status", val);
339    }
340  },
341
342  get status() {
343    return this._status;
344  },
345
346  isAllowed: function BG_sanitize(feature) {
347    return !(feature in DisallowedFeatures);
348  },
349
350  getActivePolicies() {
351    return this._parsedPolicies;
352  },
353
354  setSupportMenu(supportMenu) {
355    SupportMenu = supportMenu;
356  },
357
358  getSupportMenu() {
359    return SupportMenu;
360  },
361
362  setExtensionPolicies(extensionPolicies) {
363    ExtensionPolicies = extensionPolicies;
364  },
365
366  getExtensionPolicy(extensionID) {
367    if (ExtensionPolicies && extensionID in ExtensionPolicies) {
368      return ExtensionPolicies[extensionID];
369    }
370    return null;
371  },
372
373  setExtensionSettings(extensionSettings) {
374    ExtensionSettings = extensionSettings;
375    if (
376      "*" in extensionSettings &&
377      "install_sources" in extensionSettings["*"]
378    ) {
379      InstallSources = new MatchPatternSet(
380        extensionSettings["*"].install_sources
381      );
382    }
383  },
384
385  getExtensionSettings(extensionID) {
386    let settings = null;
387    if (ExtensionSettings) {
388      if (extensionID in ExtensionSettings) {
389        settings = ExtensionSettings[extensionID];
390      } else if ("*" in ExtensionSettings) {
391        settings = ExtensionSettings["*"];
392      }
393    }
394    return settings;
395  },
396
397  mayInstallAddon(addon) {
398    // See https://dev.chromium.org/administrators/policy-list-3/extension-settings-full
399    if (!ExtensionSettings) {
400      return true;
401    }
402    if (addon.id in ExtensionSettings) {
403      if ("installation_mode" in ExtensionSettings[addon.id]) {
404        switch (ExtensionSettings[addon.id].installation_mode) {
405          case "blocked":
406            return false;
407          default:
408            return true;
409        }
410      }
411    }
412    if ("*" in ExtensionSettings) {
413      if (
414        ExtensionSettings["*"].installation_mode &&
415        ExtensionSettings["*"].installation_mode == "blocked"
416      ) {
417        return false;
418      }
419      if ("allowed_types" in ExtensionSettings["*"]) {
420        return ExtensionSettings["*"].allowed_types.includes(addon.type);
421      }
422    }
423    return true;
424  },
425
426  allowedInstallSource(uri) {
427    return InstallSources ? InstallSources.matches(uri) : true;
428  },
429};
430
431let DisallowedFeatures = {};
432let SupportMenu = null;
433let ExtensionPolicies = null;
434let ExtensionSettings = null;
435let InstallSources = null;
436
437/**
438 * areEnterpriseOnlyPoliciesAllowed
439 *
440 * Checks whether the policies marked as enterprise_only in the
441 * schema are allowed to run on this browser.
442 *
443 * This is meant to only allow policies to run on ESR, but in practice
444 * we allow it to run on channels different than release, to allow
445 * these policies to be tested on pre-release channels.
446 *
447 * @returns {Bool} Whether the policy can run.
448 */
449function areEnterpriseOnlyPoliciesAllowed() {
450  if (Cu.isInAutomation || isXpcshell) {
451    if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
452      // This is used as an override to test the "enterprise_only"
453      // functionality itself on tests.
454      return false;
455    }
456    return true;
457  }
458
459  if (AppConstants.MOZ_UPDATE_CHANNEL != "release") {
460    return true;
461  }
462
463  return false;
464}
465
466/*
467 * JSON PROVIDER OF POLICIES
468 *
469 * This is a platform-agnostic provider which looks for
470 * policies specified through a policies.json file stored
471 * in the installation's distribution folder.
472 */
473
474class JSONPoliciesProvider {
475  constructor() {
476    this._policies = null;
477    this._readData();
478  }
479
480  get hasPolicies() {
481    return this._policies !== null && !isEmptyObject(this._policies);
482  }
483
484  get policies() {
485    return this._policies;
486  }
487
488  get failed() {
489    return this._failed;
490  }
491
492  _getConfigurationFile() {
493    let configFile = null;
494
495    if (AppConstants.platform == "linux") {
496      let systemConfigFile = Cc["@mozilla.org/file/local;1"].createInstance(
497        Ci.nsIFile
498      );
499      systemConfigFile.initWithPath(
500        "/etc/" + Services.appinfo.name.toLowerCase() + "/policies"
501      );
502      systemConfigFile.append(POLICIES_FILENAME);
503      if (systemConfigFile.exists()) {
504        return systemConfigFile;
505      }
506    }
507
508    try {
509      let perUserPath = Services.prefs.getBoolPref(PREF_PER_USER_DIR, false);
510      if (perUserPath) {
511        configFile = Services.dirsvc.get("XREUserRunTimeDir", Ci.nsIFile);
512      } else {
513        configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
514      }
515      configFile.append(POLICIES_FILENAME);
516    } catch (ex) {
517      // Getting the correct directory will fail in xpcshell tests. This should
518      // be handled the same way as if the configFile simply does not exist.
519    }
520
521    let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH, "");
522
523    // Check if we are in automation *before* we use the synchronous
524    // nsIFile.exists() function or allow the config file to be overriden
525    // An alternate policy path can also be used in Nightly builds (for
526    // testing purposes), but the Background Update Agent will be unable to
527    // detect the alternate policy file so the DisableAppUpdate policy may not
528    // work as expected.
529    if (
530      alternatePath &&
531      (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) &&
532      (!configFile || !configFile.exists())
533    ) {
534      if (alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
535        // Intentionally not using a default value on this pref lookup. If no
536        // test root is set, we are not currently testing and this function
537        // should throw rather than returning something.
538        let testRoot = Services.prefs.getStringPref(PREF_TEST_ROOT);
539        let relativePath = alternatePath.substring(
540          MAGIC_TEST_ROOT_PREFIX.length
541        );
542        if (AppConstants.platform == "win") {
543          relativePath = relativePath.replace(/\//g, "\\");
544        }
545        alternatePath = testRoot + relativePath;
546      }
547
548      configFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
549      configFile.initWithPath(alternatePath);
550    }
551
552    return configFile;
553  }
554
555  _readData() {
556    let configFile = this._getConfigurationFile();
557    if (!configFile) {
558      // Do nothing, _policies will remain null
559      return;
560    }
561    try {
562      let data = Cu.readUTF8File(configFile);
563      if (data) {
564        this._policies = JSON.parse(data).policies;
565
566        if (!this._policies) {
567          log.error("Policies file doesn't contain a 'policies' object");
568          this._failed = true;
569        }
570      }
571    } catch (ex) {
572      if (
573        ex instanceof Components.Exception &&
574        ex.result == Cr.NS_ERROR_FILE_NOT_FOUND
575      ) {
576        // Do nothing, _policies will remain null
577      } else if (ex instanceof SyntaxError) {
578        log.error(`Error parsing JSON file: ${ex}`);
579        this._failed = true;
580      } else {
581        log.error(`Error reading JSON file: ${ex}`);
582        this._failed = true;
583      }
584    }
585  }
586}
587
588class WindowsGPOPoliciesProvider {
589  constructor() {
590    this._policies = null;
591
592    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
593      Ci.nsIWindowsRegKey
594    );
595
596    // Machine policies override user policies, so we read
597    // user policies first and then replace them if necessary.
598    log.debug("root = HKEY_CURRENT_USER");
599    this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
600    // We don't access machine policies in testing
601    if (!Cu.isInAutomation && !isXpcshell) {
602      log.debug("root = HKEY_LOCAL_MACHINE");
603      this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
604    }
605  }
606
607  get hasPolicies() {
608    return this._policies !== null && !isEmptyObject(this._policies);
609  }
610
611  get policies() {
612    return this._policies;
613  }
614
615  get failed() {
616    return this._failed;
617  }
618
619  _readData(wrk, root) {
620    try {
621      let regLocation = "SOFTWARE\\Policies";
622      if (Cu.isInAutomation || isXpcshell) {
623        try {
624          regLocation = Services.prefs.getStringPref(PREF_ALTERNATE_GPO);
625        } catch (e) {}
626      }
627      wrk.open(root, regLocation, wrk.ACCESS_READ);
628      if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
629        this._policies = WindowsGPOParser.readPolicies(wrk, this._policies);
630      }
631      wrk.close();
632    } catch (e) {
633      log.error("Unable to access registry - ", e);
634    }
635  }
636}
637
638class macOSPoliciesProvider {
639  constructor() {
640    this._policies = null;
641    let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance(
642      Ci.nsIMacPreferencesReader
643    );
644    if (!prefReader.policiesEnabled()) {
645      return;
646    }
647    this._policies = macOSPoliciesParser.readPolicies(prefReader);
648  }
649
650  get hasPolicies() {
651    return this._policies !== null && Object.keys(this._policies).length;
652  }
653
654  get policies() {
655    return this._policies;
656  }
657
658  get failed() {
659    return this._failed;
660  }
661}
662
663class CombinedProvider {
664  constructor(primaryProvider, secondaryProvider) {
665    // Combine policies with primaryProvider taking precedence.
666    // We only do this for top level policies.
667    this._policies = primaryProvider._policies;
668    for (let policyName of Object.keys(secondaryProvider.policies)) {
669      if (!(policyName in this._policies)) {
670        this._policies[policyName] = secondaryProvider.policies[policyName];
671      }
672    }
673  }
674
675  get hasPolicies() {
676    // Combined provider always has policies.
677    return true;
678  }
679
680  get policies() {
681    return this._policies;
682  }
683
684  get failed() {
685    // Combined provider never fails.
686    return false;
687  }
688}
689