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