EnterprisePoliciesParent.jsm 20 KB
Newer Older
1
2
3
4
/* 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/. */

5
6
var EXPORTED_SYMBOLS = ["EnterprisePoliciesManager"];

7
8
9
10
// To ensure that policies intended for Firefox or another browser will not
// be used, Tor Browser only looks for policies in ${InstallDir}/distribution
#define AVOID_SYSTEM_POLICIES MOZ_PROXY_BYPASS_PROTECTION

11
12
13
14
15
16
17
const { XPCOMUtils } = ChromeUtils.import(
  "resource://gre/modules/XPCOMUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { AppConstants } = ChromeUtils.import(
  "resource://gre/modules/AppConstants.jsm"
);
18
19

XPCOMUtils.defineLazyModuleGetters(this, {
20
#ifndef AVOID_SYSTEM_POLICIES
21
  WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.jsm",
22
23
  macOSPoliciesParser:
    "resource://gre/modules/policies/macOSPoliciesParser.jsm",
24
#endif
25
  Policies: "resource:///modules/policies/Policies.jsm",
26
27
  JsonSchemaValidator:
    "resource://gre/modules/components-utils/JsonSchemaValidator.jsm",
28
29
30
31
32
33
});

// This is the file that will be searched for in the
// ${InstallDir}/distribution folder.
const POLICIES_FILENAME = "policies.json";

34
35
36
// When true browser policy is loaded per-user from
// /run/user/$UID/appname
const PREF_PER_USER_DIR = "toolkit.policies.perUserDir";
37
38
39
// 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
40
const PREF_ALTERNATE_PATH = "browser.policies.alternatePath";
41
42
43
// For testing GPO, you can set an alternate location in testing
const PREF_ALTERNATE_GPO = "browser.policies.alternateGPO";

44
45
46
47
// For testing, we may want to set PREF_ALTERNATE_PATH to point to a file
// relative to the test root directory. In order to enable this, the string
// below may be placed at the beginning of that preference value and it will
// be replaced with the path to the test root directory.
48
49
const MAGIC_TEST_ROOT_PREFIX = "<test-root>";
const PREF_TEST_ROOT = "mochitest.testRoot";
50

51
const PREF_LOGLEVEL = "browser.policies.loglevel";
52

53
54
55
// To force disallowing enterprise-only policies during tests
const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise";

56
57
58
// To allow for cleaning up old policies
const PREF_POLICIES_APPLIED = "browser.policies.applied";

59
XPCOMUtils.defineLazyGetter(this, "log", () => {
60
  let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm");
61
62
63
64
65
66
67
68
69
  return new ConsoleAPI({
    prefix: "Enterprise Policies",
    // tip: set maxLogLevel to "debug" and use log.debug() to create detailed
    // messages during development. See LOG_LEVELS in Console.jsm for details.
    maxLogLevel: "error",
    maxLogLevelPref: PREF_LOGLEVEL,
  });
});

70
71
72
let env = Cc["@mozilla.org/process/environment;1"].getService(
  Ci.nsIEnvironment
);
73
74
const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR");

75
76
77
78
79
80
81
82
83
84
85
86
87
88
// We're only testing for empty objects, not
// empty strings or empty arrays.
function isEmptyObject(obj) {
  if (typeof obj != "object" || Array.isArray(obj)) {
    return false;
  }
  for (let key of Object.keys(obj)) {
    if (!isEmptyObject(obj[key])) {
      return false;
    }
  }
  return true;
}

89
90
91
92
93
94
95
96
function EnterprisePoliciesManager() {
  Services.obs.addObserver(this, "profile-after-change", true);
  Services.obs.addObserver(this, "final-ui-startup", true);
  Services.obs.addObserver(this, "sessionstore-windows-restored", true);
  Services.obs.addObserver(this, "EnterprisePolicies:Restart", true);
}

EnterprisePoliciesManager.prototype = {
97
  QueryInterface: ChromeUtils.generateQI([
98
99
100
    "nsIObserver",
    "nsISupportsWeakReference",
    "nsIEnterprisePolicies",
101
  ]),
102
103

  _initialize() {
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
    if (Services.prefs.getBoolPref(PREF_POLICIES_APPLIED, false)) {
      if ("_cleanup" in Policies) {
        let policyImpl = Policies._cleanup;

        for (let timing of Object.keys(this._callbacks)) {
          let policyCallback = policyImpl[timing];
          if (policyCallback) {
            this._schedulePolicyCallback(
              timing,
              policyCallback.bind(
                policyImpl,
                this /* the EnterprisePoliciesManager */
              )
            );
          }
        }
      }
      Services.prefs.clearUserPref(PREF_POLICIES_APPLIED);
    }

124
    let provider = this._chooseProvider();
125

126
127
    if (provider.failed) {
      this.status = Ci.nsIEnterprisePolicies.FAILED;
128
129
130
      return;
    }

131
132
    if (!provider.hasPolicies) {
      this.status = Ci.nsIEnterprisePolicies.INACTIVE;
133
134
135
136
      return;
    }

    this.status = Ci.nsIEnterprisePolicies.ACTIVE;
137
    this._parsedPolicies = {};
138
139
140
141
    Services.telemetry.scalarSet(
      "policies.count",
      Object.keys(provider.policies).length
    );
142
    this._activatePolicies(provider.policies);
143
144

    Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true);
145
146
  },

147
  _chooseProvider() {
148
    let platformProvider = null;
149
#ifndef AVOID_SYSTEM_POLICIES
150
    if (AppConstants.platform == "win") {
151
      platformProvider = new WindowsGPOPoliciesProvider();
152
    } else if (AppConstants.platform == "macosx") {
153
      platformProvider = new macOSPoliciesProvider();
154
    }
155
#endif
156
157
158
159
160
161
    let jsonProvider = new JSONPoliciesProvider();
    if (platformProvider && platformProvider.hasPolicies) {
      if (jsonProvider.hasPolicies) {
        return new CombinedProvider(platformProvider, jsonProvider);
      }
      return platformProvider;
162
    }
163
    return jsonProvider;
164
165
166
  },

  _activatePolicies(unparsedPolicies) {
167
168
169
    let { schema } = ChromeUtils.import(
      "resource:///modules/policies/schema.jsm"
    );
170

171
    for (let policyName of Object.keys(unparsedPolicies)) {
172
      let policySchema = schema.properties[policyName];
173
      let policyParameters = unparsedPolicies[policyName];
174
175
176
177
178
179

      if (!policySchema) {
        log.error(`Unknown policy: ${policyName}`);
        continue;
      }

180
181
182
183
184
      if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) {
        log.error(`Policy ${policyName} is only allowed on ESR`);
        continue;
      }

185
186
187
188
189
190
      let {
        valid: parametersAreValid,
        parsedValue: parsedParameters,
      } = JsonSchemaValidator.validate(policyParameters, policySchema, {
        allowExtraProperties: true,
      });
191
192
193
194
195
196

      if (!parametersAreValid) {
        log.error(`Invalid parameters specified for ${policyName}.`);
        continue;
      }

197
      this._parsedPolicies[policyName] = parsedParameters;
198
199
200
      let policyImpl = Policies[policyName];

      for (let timing of Object.keys(this._callbacks)) {
201
        let policyCallback = policyImpl[timing];
202
203
204
        if (policyCallback) {
          this._schedulePolicyCallback(
            timing,
205
206
207
208
209
210
            policyCallback.bind(
              policyImpl,
              this /* the EnterprisePoliciesManager */,
              parsedParameters
            )
          );
211
212
213
214
215
216
        }
      }
    }
  },

  _callbacks: {
217
    // The earliest that a policy callback can run. This will
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
    // happen right after the Policy Engine itself has started,
    // and before the Add-ons Manager has started.
    onBeforeAddons: [],

    // This happens after all the initialization related to
    // the profile has finished (prefs, places database, etc.).
    onProfileAfterChange: [],

    // Just before the first browser window gets created.
    onBeforeUIStartup: [],

    // Called after all windows from the last session have been
    // restored (or the default window and homepage tab, if the
    // session is not being restored).
    // The content of the tabs themselves have not necessarily
    // finished loading.
    onAllWindowsRestored: [],
235
236
237
238
239
240
241
242
  },

  _schedulePolicyCallback(timing, callback) {
    this._callbacks[timing].push(callback);
  },

  _runPoliciesCallbacks(timing) {
    let callbacks = this._callbacks[timing];
243
    while (callbacks.length) {
244
245
246
247
248
249
250
251
252
253
254
255
      let callback = callbacks.shift();
      try {
        callback();
      } catch (ex) {
        log.error("Error running ", callback, `for ${timing}:`, ex);
      }
    }
  },

  async _restart() {
    DisallowedFeatures = {};

256
257
258
    Services.ppmm.sharedData.delete("EnterprisePolicies:Status");
    Services.ppmm.sharedData.delete("EnterprisePolicies:DisallowedFeatures");

259
260
261
262
263
    this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED;
    for (let timing of Object.keys(this._callbacks)) {
      this._callbacks[timing] = [];
    }

264
265
266
    let { PromiseUtils } = ChromeUtils.import(
      "resource://gre/modules/PromiseUtils.jsm"
    );
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
    // Simulate the startup process. This step-by-step is a bit ugly but it
    // tries to emulate the same behavior as of a normal startup.

    await PromiseUtils.idleDispatch(() => {
      this.observe(null, "policies-startup", null);
    });

    await PromiseUtils.idleDispatch(() => {
      this.observe(null, "profile-after-change", null);
    });

    await PromiseUtils.idleDispatch(() => {
      this.observe(null, "final-ui-startup", null);
    });

    await PromiseUtils.idleDispatch(() => {
      this.observe(null, "sessionstore-windows-restored", null);
    });
  },

  // nsIObserver implementation
  observe: function BG_observe(subject, topic, data) {
    switch (topic) {
      case "policies-startup":
291
292
        // Before the first set of policy callbacks runs, we must
        // initialize the service.
293
        this._initialize();
294
295

        this._runPoliciesCallbacks("onBeforeAddons");
296
297
298
        break;

      case "profile-after-change":
299
        this._runPoliciesCallbacks("onProfileAfterChange");
300
301
302
        break;

      case "final-ui-startup":
303
        this._runPoliciesCallbacks("onBeforeUIStartup");
304
305
306
        break;

      case "sessionstore-windows-restored":
307
        this._runPoliciesCallbacks("onAllWindowsRestored");
308
309

        // After the last set of policy callbacks ran, notify the test observer.
310
311
312
313
        Services.obs.notifyObservers(
          null,
          "EnterprisePolicies:AllPoliciesApplied"
        );
314
315
316
317
318
319
320
321
322
        break;

      case "EnterprisePolicies:Restart":
        this._restart().then(null, Cu.reportError);
        break;
    }
  },

  disallowFeature(feature, neededOnContentProcess = false) {
323
    DisallowedFeatures[feature] = neededOnContentProcess;
324
325
326
327

    // NOTE: For optimization purposes, only features marked as needed
    // on content process will be passed onto the child processes.
    if (neededOnContentProcess) {
328
329
330
331
332
333
      Services.ppmm.sharedData.set(
        "EnterprisePolicies:DisallowedFeatures",
        new Set(
          Object.keys(DisallowedFeatures).filter(key => DisallowedFeatures[key])
        )
      );
334
335
336
337
338
339
340
341
342
343
344
345
    }
  },

  // ------------------------------
  // public nsIEnterprisePolicies members
  // ------------------------------

  _status: Ci.nsIEnterprisePolicies.UNINITIALIZED,

  set status(val) {
    this._status = val;
    if (val != Ci.nsIEnterprisePolicies.INACTIVE) {
346
      Services.ppmm.sharedData.set("EnterprisePolicies:Status", val);
347
348
349
350
351
352
353
354
355
356
    }
  },

  get status() {
    return this._status;
  },

  isAllowed: function BG_sanitize(feature) {
    return !(feature in DisallowedFeatures);
  },
357
358
359
360

  getActivePolicies() {
    return this._parsedPolicies;
  },
361
362
363
364
365
366
367
368

  setSupportMenu(supportMenu) {
    SupportMenu = supportMenu;
  },

  getSupportMenu() {
    return SupportMenu;
  },
369
370
371
372
373
374

  setExtensionPolicies(extensionPolicies) {
    ExtensionPolicies = extensionPolicies;
  },

  getExtensionPolicy(extensionID) {
375
    if (ExtensionPolicies && extensionID in ExtensionPolicies) {
376
377
378
379
      return ExtensionPolicies[extensionID];
    }
    return null;
  },
380
381
382

  setExtensionSettings(extensionSettings) {
    ExtensionSettings = extensionSettings;
383
384
385
386
387
388
389
    if (
      "*" in extensionSettings &&
      "install_sources" in extensionSettings["*"]
    ) {
      InstallSources = new MatchPatternSet(
        extensionSettings["*"].install_sources
      );
390
    }
391
392
393
394
  },

  getExtensionSettings(extensionID) {
    let settings = null;
395
396
397
398
399
400
    if (ExtensionSettings) {
      if (extensionID in ExtensionSettings) {
        settings = ExtensionSettings[extensionID];
      } else if ("*" in ExtensionSettings) {
        settings = ExtensionSettings["*"];
      }
401
402
403
    }
    return settings;
  },
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420

  mayInstallAddon(addon) {
    // See https://dev.chromium.org/administrators/policy-list-3/extension-settings-full
    if (!ExtensionSettings) {
      return true;
    }
    if (addon.id in ExtensionSettings) {
      if ("installation_mode" in ExtensionSettings[addon.id]) {
        switch (ExtensionSettings[addon.id].installation_mode) {
          case "blocked":
            return false;
          default:
            return true;
        }
      }
    }
    if ("*" in ExtensionSettings) {
421
422
423
424
      if (
        ExtensionSettings["*"].installation_mode &&
        ExtensionSettings["*"].installation_mode == "blocked"
      ) {
425
426
427
428
429
430
431
432
433
434
435
436
        return false;
      }
      if ("allowed_types" in ExtensionSettings["*"]) {
        return ExtensionSettings["*"].allowed_types.includes(addon.type);
      }
    }
    return true;
  },

  allowedInstallSource(uri) {
    return InstallSources ? InstallSources.matches(uri) : true;
  },
437
438
439
};

let DisallowedFeatures = {};
440
let SupportMenu = null;
441
let ExtensionPolicies = null;
442
let ExtensionSettings = null;
443
let InstallSources = null;
444

445
446
447
448
449
450
451
452
453
454
455
456
457
/**
 * areEnterpriseOnlyPoliciesAllowed
 *
 * Checks whether the policies marked as enterprise_only in the
 * schema are allowed to run on this browser.
 *
 * This is meant to only allow policies to run on ESR, but in practice
 * we allow it to run on channels different than release, to allow
 * these policies to be tested on pre-release channels.
 *
 * @returns {Bool} Whether the policy can run.
 */
function areEnterpriseOnlyPoliciesAllowed() {
458
459
460
461
462
463
464
  if (Cu.isInAutomation || isXpcshell) {
    if (Services.prefs.getBoolPref(PREF_DISALLOW_ENTERPRISE, false)) {
      // This is used as an override to test the "enterprise_only"
      // functionality itself on tests.
      return false;
    }
    return true;
465
466
  }

467
  if (AppConstants.MOZ_UPDATE_CHANNEL != "release") {
468
469
470
471
472
    return true;
  }

  return false;
}
473

474
475
476
477
478
479
480
/*
 * JSON PROVIDER OF POLICIES
 *
 * This is a platform-agnostic provider which looks for
 * policies specified through a policies.json file stored
 * in the installation's distribution folder.
 */
481

482
483
484
485
486
class JSONPoliciesProvider {
  constructor() {
    this._policies = null;
    this._readData();
  }
487

488
  get hasPolicies() {
489
    return this._policies !== null && !isEmptyObject(this._policies);
490
  }
491

492
493
494
495
496
497
498
  get policies() {
    return this._policies;
  }

  get failed() {
    return this._failed;
  }
499

500
  _getConfigurationFile() {
501
    let configFile = null;
502
#ifndef AVOID_SYSTEM_POLICIES
503
504
505
506
507
508
509
510
511
512
513
514
    if (AppConstants.platform == "linux") {
      let systemConfigFile = Cc["@mozilla.org/file/local;1"].createInstance(
        Ci.nsIFile
      );
      systemConfigFile.initWithPath(
        "/etc/" + Services.appinfo.name.toLowerCase() + "/policies"
      );
      systemConfigFile.append(POLICIES_FILENAME);
      if (systemConfigFile.exists()) {
        return systemConfigFile;
      }
    }
515
#endif
516
    try {
517
518
519
520
521
522
      let perUserPath = Services.prefs.getBoolPref(PREF_PER_USER_DIR, false);
      if (perUserPath) {
        configFile = Services.dirsvc.get("XREUserRunTimeDir", Ci.nsIFile);
      } else {
        configFile = Services.dirsvc.get("XREAppDist", Ci.nsIFile);
      }
523
524
525
526
527
      configFile.append(POLICIES_FILENAME);
    } catch (ex) {
      // Getting the correct directory will fail in xpcshell tests. This should
      // be handled the same way as if the configFile simply does not exist.
    }
528
529
530

    let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH, "");

531
532
533
534
535
536
    // Check if we are in automation *before* we use the synchronous
    // nsIFile.exists() function or allow the config file to be overriden
    // An alternate policy path can also be used in Nightly builds (for
    // testing purposes), but the Background Update Agent will be unable to
    // detect the alternate policy file so the DisableAppUpdate policy may not
    // work as expected.
537
538
539
540
541
    if (
      alternatePath &&
      (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) &&
      (!configFile || !configFile.exists())
    ) {
542
543
544
545
546
      if (alternatePath.startsWith(MAGIC_TEST_ROOT_PREFIX)) {
        // Intentionally not using a default value on this pref lookup. If no
        // test root is set, we are not currently testing and this function
        // should throw rather than returning something.
        let testRoot = Services.prefs.getStringPref(PREF_TEST_ROOT);
547
548
549
        let relativePath = alternatePath.substring(
          MAGIC_TEST_ROOT_PREFIX.length
        );
550
551
552
553
554
555
        if (AppConstants.platform == "win") {
          relativePath = relativePath.replace(/\//g, "\\");
        }
        alternatePath = testRoot + relativePath;
      }

556
      configFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
557
      configFile.initWithPath(alternatePath);
558
559
    }

560
561
    return configFile;
  }
562

563
  _readData() {
564
565
566
567
568
    let configFile = this._getConfigurationFile();
    if (!configFile) {
      // Do nothing, _policies will remain null
      return;
    }
569
    try {
570
      let data = Cu.readUTF8File(configFile);
571
      if (data) {
572
        this._policies = JSON.parse(data).policies;
573
574
575
576
577

        if (!this._policies) {
          log.error("Policies file doesn't contain a 'policies' object");
          this._failed = true;
        }
578
579
      }
    } catch (ex) {
580
581
582
583
      if (
        ex instanceof Components.Exception &&
        ex.result == Cr.NS_ERROR_FILE_NOT_FOUND
      ) {
584
        // Do nothing, _policies will remain null
585
586
      } else if (ex instanceof SyntaxError) {
        log.error("Error parsing JSON file");
587
        this._failed = true;
588
589
      } else {
        log.error("Error reading file");
590
        this._failed = true;
591
592
593
      }
    }
  }
594
}
595

596
#ifndef AVOID_SYSTEM_POLICIES
597
class WindowsGPOPoliciesProvider {
598
599
600
  constructor() {
    this._policies = null;

601
602
603
    let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
      Ci.nsIWindowsRegKey
    );
604

605
606
    // Machine policies override user policies, so we read
    // user policies first and then replace them if necessary.
607
    log.debug("root = HKEY_CURRENT_USER");
608
    this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER);
609
610
611
612
613
    // We don't access machine policies in testing
    if (!Cu.isInAutomation && !isXpcshell) {
      log.debug("root = HKEY_LOCAL_MACHINE");
      this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE);
    }
614
615
616
  }

  get hasPolicies() {
617
    return this._policies !== null && !isEmptyObject(this._policies);
618
619
620
621
622
623
624
625
626
627
  }

  get policies() {
    return this._policies;
  }

  get failed() {
    return this._failed;
  }

628
  _readData(wrk, root) {
629
    try {
630
631
632
633
634
635
636
      let regLocation = "SOFTWARE\\Policies";
      if (Cu.isInAutomation || isXpcshell) {
        try {
          regLocation = Services.prefs.getStringPref(PREF_ALTERNATE_GPO);
        } catch (e) {}
      }
      wrk.open(root, regLocation, wrk.ACCESS_READ);
637
638
639
640
641
642
      if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) {
        this._policies = WindowsGPOParser.readPolicies(wrk, this._policies);
      }
      wrk.close();
    } catch (e) {
      log.error("Unable to access registry - ", e);
643
    }
644
645
  }
}
646

647
648
class macOSPoliciesProvider {
  constructor() {
649
    this._policies = null;
650
651
652
    let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance(
      Ci.nsIMacPreferencesReader
    );
653
654
655
656
657
658
659
    if (!prefReader.policiesEnabled()) {
      return;
    }
    this._policies = macOSPoliciesParser.readPolicies(prefReader);
  }

  get hasPolicies() {
660
    return this._policies !== null && Object.keys(this._policies).length;
661
662
663
664
665
666
667
668
669
670
  }

  get policies() {
    return this._policies;
  }

  get failed() {
    return this._failed;
  }
}
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697

class CombinedProvider {
  constructor(primaryProvider, secondaryProvider) {
    // Combine policies with primaryProvider taking precedence.
    // We only do this for top level policies.
    this._policies = primaryProvider._policies;
    for (let policyName of Object.keys(secondaryProvider.policies)) {
      if (!(policyName in this._policies)) {
        this._policies[policyName] = secondaryProvider.policies[policyName];
      }
    }
  }

  get hasPolicies() {
    // Combined provider always has policies.
    return true;
  }

  get policies() {
    return this._policies;
  }

  get failed() {
    // Combined provider never fails.
    return false;
  }
}
698
#endif