/* 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/. */ var EXPORTED_SYMBOLS = ["EnterprisePoliciesManager"]; 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" ); XPCOMUtils.defineLazyModuleGetters(this, { WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.jsm", macOSPoliciesParser: "resource://gre/modules/policies/macOSPoliciesParser.jsm", Policies: "resource:///modules/policies/Policies.jsm", JsonSchemaValidator: "resource://gre/modules/components-utils/JsonSchemaValidator.jsm", }); // This is the file that will be searched for in the // ${InstallDir}/distribution folder. const POLICIES_FILENAME = "policies.json"; // When true browser policy is loaded per-user from // /run/user/$UID/appname const PREF_PER_USER_DIR = "toolkit.policies.perUserDir"; // 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 const PREF_ALTERNATE_PATH = "browser.policies.alternatePath"; // 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. const MAGIC_TEST_ROOT_PREFIX = ""; const PREF_TEST_ROOT = "mochitest.testRoot"; const PREF_LOGLEVEL = "browser.policies.loglevel"; // To force disallowing enterprise-only policies during tests const PREF_DISALLOW_ENTERPRISE = "browser.policies.testing.disallowEnterprise"; // To allow for cleaning up old policies const PREF_POLICIES_APPLIED = "browser.policies.applied"; XPCOMUtils.defineLazyGetter(this, "log", () => { let { ConsoleAPI } = ChromeUtils.import("resource://gre/modules/Console.jsm"); 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, }); }); let env = Cc["@mozilla.org/process/environment;1"].getService( Ci.nsIEnvironment ); const isXpcshell = env.exists("XPCSHELL_TEST_PROFILE_DIR"); // 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; } 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 = { QueryInterface: ChromeUtils.generateQI([ "nsIObserver", "nsISupportsWeakReference", "nsIEnterprisePolicies", ]), _initialize() { 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); } let provider = this._chooseProvider(); if (!provider) { this.status = Ci.nsIEnterprisePolicies.INACTIVE; return; } if (provider.failed) { this.status = Ci.nsIEnterprisePolicies.FAILED; return; } this.status = Ci.nsIEnterprisePolicies.ACTIVE; this._parsedPolicies = {}; Services.telemetry.scalarSet( "policies.count", Object.keys(provider.policies).length ); this._activatePolicies(provider.policies); Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true); }, _chooseProvider() { let provider = null; if (AppConstants.platform == "win") { provider = new WindowsGPOPoliciesProvider(); } else if (AppConstants.platform == "macosx") { provider = new macOSPoliciesProvider(); } if (provider && provider.hasPolicies) { return provider; } provider = new JSONPoliciesProvider(); if (provider.hasPolicies) { return provider; } return null; }, _activatePolicies(unparsedPolicies) { let { schema } = ChromeUtils.import( "resource:///modules/policies/schema.jsm" ); for (let policyName of Object.keys(unparsedPolicies)) { let policySchema = schema.properties[policyName]; let policyParameters = unparsedPolicies[policyName]; if (!policySchema) { log.error(`Unknown policy: ${policyName}`); continue; } if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) { log.error(`Policy ${policyName} is only allowed on ESR`); continue; } let { valid: parametersAreValid, parsedValue: parsedParameters, } = JsonSchemaValidator.validate(policyParameters, policySchema, { allowExtraProperties: true, }); if (!parametersAreValid) { log.error(`Invalid parameters specified for ${policyName}.`); continue; } this._parsedPolicies[policyName] = parsedParameters; let policyImpl = Policies[policyName]; for (let timing of Object.keys(this._callbacks)) { let policyCallback = policyImpl[timing]; if (policyCallback) { this._schedulePolicyCallback( timing, policyCallback.bind( policyImpl, this /* the EnterprisePoliciesManager */, parsedParameters ) ); } } } }, _callbacks: { // The earliest that a policy callback can run. This will // 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: [], }, _schedulePolicyCallback(timing, callback) { this._callbacks[timing].push(callback); }, _runPoliciesCallbacks(timing) { let callbacks = this._callbacks[timing]; while (callbacks.length) { let callback = callbacks.shift(); try { callback(); } catch (ex) { log.error("Error running ", callback, `for ${timing}:`, ex); } } }, async _restart() { DisallowedFeatures = {}; Services.ppmm.sharedData.delete("EnterprisePolicies:Status"); Services.ppmm.sharedData.delete("EnterprisePolicies:DisallowedFeatures"); this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED; for (let timing of Object.keys(this._callbacks)) { this._callbacks[timing] = []; } let { PromiseUtils } = ChromeUtils.import( "resource://gre/modules/PromiseUtils.jsm" ); // 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": // Before the first set of policy callbacks runs, we must // initialize the service. this._initialize(); this._runPoliciesCallbacks("onBeforeAddons"); break; case "profile-after-change": this._runPoliciesCallbacks("onProfileAfterChange"); break; case "final-ui-startup": this._runPoliciesCallbacks("onBeforeUIStartup"); break; case "sessionstore-windows-restored": this._runPoliciesCallbacks("onAllWindowsRestored"); // After the last set of policy callbacks ran, notify the test observer. Services.obs.notifyObservers( null, "EnterprisePolicies:AllPoliciesApplied" ); break; case "EnterprisePolicies:Restart": this._restart().then(null, Cu.reportError); break; } }, disallowFeature(feature, neededOnContentProcess = false) { DisallowedFeatures[feature] = neededOnContentProcess; // NOTE: For optimization purposes, only features marked as needed // on content process will be passed onto the child processes. if (neededOnContentProcess) { Services.ppmm.sharedData.set( "EnterprisePolicies:DisallowedFeatures", new Set( Object.keys(DisallowedFeatures).filter(key => DisallowedFeatures[key]) ) ); } }, // ------------------------------ // public nsIEnterprisePolicies members // ------------------------------ _status: Ci.nsIEnterprisePolicies.UNINITIALIZED, set status(val) { this._status = val; if (val != Ci.nsIEnterprisePolicies.INACTIVE) { Services.ppmm.sharedData.set("EnterprisePolicies:Status", val); } return val; }, get status() { return this._status; }, isAllowed: function BG_sanitize(feature) { return !(feature in DisallowedFeatures); }, getActivePolicies() { return this._parsedPolicies; }, setSupportMenu(supportMenu) { SupportMenu = supportMenu; }, getSupportMenu() { return SupportMenu; }, setExtensionPolicies(extensionPolicies) { ExtensionPolicies = extensionPolicies; }, getExtensionPolicy(extensionID) { if (ExtensionPolicies && extensionID in ExtensionPolicies) { return ExtensionPolicies[extensionID]; } return null; }, setExtensionSettings(extensionSettings) { ExtensionSettings = extensionSettings; if ( "*" in extensionSettings && "install_sources" in extensionSettings["*"] ) { InstallSources = new MatchPatternSet( extensionSettings["*"].install_sources ); } }, getExtensionSettings(extensionID) { let settings = null; if (ExtensionSettings) { if (extensionID in ExtensionSettings) { settings = ExtensionSettings[extensionID]; } else if ("*" in ExtensionSettings) { settings = ExtensionSettings["*"]; } } return settings; }, 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) { if ( ExtensionSettings["*"].installation_mode && ExtensionSettings["*"].installation_mode == "blocked" ) { return false; } if ("allowed_types" in ExtensionSettings["*"]) { return ExtensionSettings["*"].allowed_types.includes(addon.type); } } return true; }, allowedInstallSource(uri) { return InstallSources ? InstallSources.matches(uri) : true; }, }; let DisallowedFeatures = {}; let SupportMenu = null; let ExtensionPolicies = null; let ExtensionSettings = null; let InstallSources = null; /** * 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() { 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; } if (AppConstants.MOZ_UPDATE_CHANNEL != "release") { return true; } return false; } /* * 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. */ class JSONPoliciesProvider { constructor() { this._policies = null; this._failed = false; this._readData(); } get hasPolicies() { return ( this._failed || (this._policies !== null && !isEmptyObject(this._policies)) ); } get policies() { return this._policies; } get failed() { return this._failed; } _getConfigurationFile() { let configFile = null; 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; } } try { 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); } 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. } let alternatePath = Services.prefs.getStringPref(PREF_ALTERNATE_PATH, ""); // 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. if ( alternatePath && (Cu.isInAutomation || AppConstants.NIGHTLY_BUILD || isXpcshell) && (!configFile || !configFile.exists()) ) { 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); let relativePath = alternatePath.substring( MAGIC_TEST_ROOT_PREFIX.length ); if (AppConstants.platform == "win") { relativePath = relativePath.replace(/\//g, "\\"); } alternatePath = testRoot + relativePath; } configFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile); configFile.initWithPath(alternatePath); } return configFile; } _readData() { let configFile = this._getConfigurationFile(); if (!configFile) { // Do nothing, _policies will remain null return; } try { let data = Cu.readUTF8File(configFile); if (data) { this._policies = JSON.parse(data).policies; if (!this._policies) { log.error("Policies file doesn't contain a 'policies' object"); this._failed = true; } } } catch (ex) { if ( ex instanceof Components.Exception && ex.result == Cr.NS_ERROR_FILE_NOT_FOUND ) { // Do nothing, _policies will remain null } else if (ex instanceof SyntaxError) { log.error("Error parsing JSON file"); this._failed = true; } else { log.error("Error reading file"); this._failed = true; } } } } class WindowsGPOPoliciesProvider { constructor() { this._policies = null; let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( Ci.nsIWindowsRegKey ); // Machine policies override user policies, so we read // user policies first and then replace them if necessary. log.debug("root = HKEY_CURRENT_USER"); this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER); log.debug("root = HKEY_LOCAL_MACHINE"); this._readData(wrk, wrk.ROOT_KEY_LOCAL_MACHINE); } get hasPolicies() { return this._policies !== null && !isEmptyObject(this._policies); } get policies() { return this._policies; } get failed() { return this._failed; } _readData(wrk, root) { try { wrk.open(root, "SOFTWARE\\Policies", wrk.ACCESS_READ); 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); } } } class macOSPoliciesProvider { constructor() { this._policies = null; let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance( Ci.nsIMacPreferencesReader ); if (!prefReader.policiesEnabled()) { return; } this._policies = macOSPoliciesParser.readPolicies(prefReader); } get hasPolicies() { return this._policies !== null && Object.keys(this._policies).length; } get policies() { return this._policies; } get failed() { return this._failed; } }