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