/* 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/. */ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs"; const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { JsonSchemaValidator: "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs", Policies: "resource:///modules/policies/Policies.sys.mjs", WindowsGPOParser: "resource://gre/modules/policies/WindowsGPOParser.sys.mjs", macOSPoliciesParser: "resource://gre/modules/policies/macOSPoliciesParser.sys.mjs", }); // 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 GPO, you can set an alternate location in testing const PREF_ALTERNATE_GPO = "browser.policies.alternateGPO"; // 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"; ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" ); 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.sys.mjs for details. maxLogLevel: "error", maxLogLevelPref: PREF_LOGLEVEL, }); }); const isXpcshell = Services.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; } export 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 lazy.Policies) { let policyImpl = lazy.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.failed) { this.status = Ci.nsIEnterprisePolicies.FAILED; this._reportEnterpriseTelemetry(); return; } if (!provider.hasPolicies) { this.status = Ci.nsIEnterprisePolicies.INACTIVE; this._reportEnterpriseTelemetry(); return; } this.status = Ci.nsIEnterprisePolicies.ACTIVE; this._parsedPolicies = {}; this._reportEnterpriseTelemetry(provider.policies); this._activatePolicies(provider.policies); Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true); }, _reportEnterpriseTelemetry(policies = {}) { let excludedDistributionIDs = [ "mozilla-mac-eol-esr115", "mozilla-win-eol-esr115", ]; let distroId = Services.prefs .getDefaultBranch(null) .getCharPref("distribution.id", ""); let policiesLength = Object.keys(policies).length; Services.telemetry.scalarSet("policies.count", policiesLength); let isEnterprise = // As we migrate folks to ESR for other reasons (deprecating an OS), // we need to add checks here for distribution IDs. (AppConstants.IS_ESR && !excludedDistributionIDs.includes(distroId)) || // If there are multiple policies then its enterprise. policiesLength > 1 || // If ImportEnterpriseRoots isn't the only policy then it's enterprise. (policiesLength && !policies.Certificates?.ImportEnterpriseRoots); Services.telemetry.scalarSet("policies.is_enterprise", isEnterprise); }, _chooseProvider() { let platformProvider = null; if (AppConstants.platform == "win" && AppConstants.MOZ_SYSTEM_POLICIES) { platformProvider = new WindowsGPOPoliciesProvider(); } else if ( AppConstants.platform == "macosx" && AppConstants.MOZ_SYSTEM_POLICIES ) { platformProvider = new macOSPoliciesProvider(); } let jsonProvider = new JSONPoliciesProvider(); if (platformProvider && platformProvider.hasPolicies) { if (jsonProvider.hasPolicies) { return new CombinedProvider(platformProvider, jsonProvider); } return platformProvider; } return jsonProvider; }, _activatePolicies(unparsedPolicies) { let { schema } = ChromeUtils.importESModule( "resource:///modules/policies/schema.sys.mjs" ); for (let policyName of Object.keys(unparsedPolicies)) { let policySchema = schema.properties[policyName]; let policyParameters = unparsedPolicies[policyName]; if (!policySchema) { lazy.log.error(`Unknown policy: ${policyName}`); continue; } if (policySchema.enterprise_only && !areEnterpriseOnlyPoliciesAllowed()) { lazy.log.error(`Policy ${policyName} is only allowed on ESR`); continue; } let { valid: parametersAreValid, parsedValue: parsedParameters } = lazy.JsonSchemaValidator.validate(policyParameters, policySchema, { allowAdditionalProperties: true, }); if (!parametersAreValid) { lazy.log.error(`Invalid parameters specified for ${policyName}.`); continue; } let policyImpl = lazy.Policies[policyName]; if (policyImpl.validate && !policyImpl.validate(parsedParameters)) { lazy.log.error( `Parameters for ${policyName} did not validate successfully.` ); continue; } this._parsedPolicies[policyName] = parsedParameters; 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) { lazy.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; this._parsedPolicies = undefined; for (let timing of Object.keys(this._callbacks)) { this._callbacks[timing] = []; } // 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. let notifyTopicOnIdle = topic => new Promise(resolve => { ChromeUtils.idleDispatch(() => { this.observe(null, topic, ""); resolve(); }); }); await notifyTopicOnIdle("policies-startup"); await notifyTopicOnIdle("profile-after-change"); await notifyTopicOnIdle("final-ui-startup"); await notifyTopicOnIdle("sessionstore-windows-restored"); }, // nsIObserver implementation observe: function BG_observe(subject, topic) { 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, console.error); 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); } }, 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; }, isExemptExecutableExtension(url, extension) { let urlObject; try { urlObject = new URL(url); } catch (e) { return false; } let { hostname } = urlObject; let exemptArray = this.getActivePolicies() ?.ExemptDomainFileTypePairsFromFileTypeDownloadWarnings; if (!hostname || !extension || !exemptArray) { return false; } extension = extension.toLowerCase(); let domains = exemptArray .filter(item => item.file_extension.toLowerCase() == extension) .map(item => item.domains) .flat(); for (let domain of domains) { if (Services.eTLD.hasRootDomain(hostname, domain)) { return true; } } return false; }, }; 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; } return ( AppConstants.IS_ESR || AppConstants.MOZ_DEV_EDITION || AppConstants.NIGHTLY_BUILD ); } /* * 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._readData(); } get hasPolicies() { return this._policies !== null && !isEmptyObject(this._policies); } get policies() { return this._policies; } get failed() { return this._failed; } _getConfigurationFile() { let configFile = null; if (AppConstants.platform == "linux" && AppConstants.MOZ_SYSTEM_POLICIES) { let systemConfigFile = Services.dirsvc.get("SysConfD", Ci.nsIFile); systemConfigFile.append("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) { lazy.log.debug(`policies.json path = ${configFile.path}`); lazy.log.debug(`policies.json content = ${data}`); this._policies = JSON.parse(data).policies; if (!this._policies) { lazy.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) { lazy.log.error(`Error parsing JSON file: ${ex}`); this._failed = true; } else { lazy.log.error(`Error reading JSON file: ${ex}`); 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. this._readData(wrk, wrk.ROOT_KEY_CURRENT_USER); // We don't access machine policies in testing if (!Cu.isInAutomation && !isXpcshell) { 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 { 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); if (wrk.hasChild("Mozilla\\" + Services.appinfo.name)) { lazy.log.debug( `root = ${ root == wrk.ROOT_KEY_CURRENT_USER ? "HKEY_CURRENT_USER" : "HKEY_LOCAL_MACHINE" }` ); this._policies = lazy.WindowsGPOParser.readPolicies( wrk, this._policies ); } wrk.close(); } catch (e) { lazy.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 = lazy.macOSPoliciesParser.readPolicies(prefReader); } get hasPolicies() { return this._policies !== null && Object.keys(this._policies).length; } get policies() { return this._policies; } get failed() { return this._failed; } } 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; } }