diff --git a/.prettierignore b/.prettierignore index 6a57ff28aad79..53982a86c4bbb 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1240,6 +1240,7 @@ toolkit/modules/AppConstants.sys.mjs # Files with MOZ_ENTERPRISE preprocessor directives browser/components/enterprisepolicies/helpers/WebsiteFilter.sys.mjs +browser/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs services/fxaccounts/FxAccounts.sys.mjs services/fxaccounts/FxAccountsClient.sys.mjs toolkit/components/downloads/DownloadCore.sys.mjs diff --git a/browser/branding/enterprise/pref/firefox-enterprise.js b/browser/branding/enterprise/pref/firefox-enterprise.js index 8fbe25c9ae0fc..523109f4e3ed5 100644 --- a/browser/branding/enterprise/pref/firefox-enterprise.js +++ b/browser/branding/enterprise/pref/firefox-enterprise.js @@ -2,7 +2,7 @@ * 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/. */ -/* global pref */ +/* Preferences here are going to be applied both to Felt and to the launched Firefox. */ pref("enterprise.console.address", "https://console.enterfox.eu"); diff --git a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs index 85ab0258b39c7..f061fe661a7c8 100644 --- a/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs +++ b/toolkit/components/enterprisepolicies/EnterprisePoliciesParent.sys.mjs @@ -46,6 +46,8 @@ const PREF_LOGLEVEL = "browser.policies.loglevel"; // To allow for cleaning up old policies const PREF_POLICIES_APPLIED = "browser.policies.applied"; +const PREF_REMOTE_POLICIES_ENABLED = "browser.policies.remote.enabled"; + ChromeUtils.defineLazyGetter(lazy, "log", () => { let { ConsoleAPI } = ChromeUtils.importESModule( "resource://gre/modules/Console.sys.mjs" @@ -79,8 +81,9 @@ 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:Reset", true); Services.obs.addObserver(this, "EnterprisePolicies:Restart", true); - Services.obs.addObserver(this, "EnterprisePolicies:Activate", true); + Services.obs.addObserver(this, "EnterprisePolicies:Update", true); Services.obs.addObserver(this, "distribution-customization-complete", true); } @@ -91,12 +94,24 @@ EnterprisePoliciesManager.prototype = { "nsIEnterprisePolicies", ]), + // Single or combined provider + _provider: null, + + // Caches latest set of parsed policies + _parsedPolicies: {}, + + isRemotePoliciesSupported() { + return ( + AppConstants.MOZ_ENTERPRISE && + Services.prefs.getBoolPref(PREF_REMOTE_POLICIES_ENABLED, false) + ); + }, + _cleanupPolicies() { - this._previousPolicies = {}; if (Services.prefs.getBoolPref(PREF_POLICIES_APPLIED, false)) { if ("_cleanup" in lazy.Policies) { let policyImpl = lazy.Policies._cleanup; - this._maybeCallbackPolicy(policyImpl); + this._scheduleActivationPolicyCallbacks(policyImpl); } Services.prefs.clearUserPref(PREF_POLICIES_APPLIED); } @@ -105,42 +120,62 @@ EnterprisePoliciesManager.prototype = { _initialize() { this._cleanupPolicies(); - const changesHandler = provider => { - if (!provider.hasPolicies) { - this._status = Ci.nsIEnterprisePolicies.INACTIVE; - Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, false); - return; - } - - // Because security.enterprise_roots.enabled is true by default, we can - // ignore attempts by Antivirus to try to set it via policy. - // We have to explicitly check for true or 1 because this happens before - // policy is parsed against the schema, so the value could be coming - // from the registry. - if ( - provider.policies && - Object.keys(provider.policies).length === 1 && - provider.policies.Certificates && - Object.keys(provider.policies.Certificates).length === 1 && - (provider.policies.Certificates.ImportEnterpriseRoots === true || - provider.policies.Certificates.ImportEnterpriseRoots === 1) - ) { - this._status = Ci.nsIEnterprisePolicies.INACTIVE; - return; - } - - this._status = Ci.nsIEnterprisePolicies.ACTIVE; - this._activatePolicies(provider.policies); - Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, true); - }; + this._policiesSchema = ChromeUtils.importESModule( + "resource:///modules/policies/schema.sys.mjs" + ).schema; this._status = Ci.nsIEnterprisePolicies.INACTIVE; Services.prefs.setBoolPref(PREF_POLICIES_APPLIED, false); - let provider = this._chooseProvider(changesHandler); - if (provider.failed) { + const localProvider = this._chooseProvider(); + if (this.isRemotePoliciesSupported()) { + const remoteProvider = RemotePoliciesProvider.getInstance(); + try { + // Poll and ingest initial set of policies + const remotePoliciesPromise = remoteProvider.ingestPolicies(); + this.spinResolve(remotePoliciesPromise); + // Will apply policy updates once policies manager is initialized + remoteProvider.startPolling(); + } catch (e) { + console.error("Unable to find policies in payload."); + } + if (localProvider.hasPolicies) { + this._provider = new CombinedProvider(remoteProvider, localProvider); + } else { + this._provider = remoteProvider; + } + } else { + this._provider = localProvider; + } + + if (this._provider.failed) { this._status = Ci.nsIEnterprisePolicies.FAILED; + return; + } + + if (!this._provider.hasPolicies) { + this._status = Ci.nsIEnterprisePolicies.INACTIVE; + return; } + + // Because security.enterprise_roots.enabled is true by default, we can + // ignore attempts by Antivirus to try to set it via policy. + // We have to explicitly check for true or 1 because this happens before + // policy is parsed against the schema, so the value could be coming + // from the registry. + const policies = this._provider.policies; + if ( + Object.keys(policies).length === 1 && + policies.Certificates && + Object.keys(policies.Certificates).length === 1 && + (policies.Certificates.ImportEnterpriseRoots === true || + policies.Certificates.ImportEnterpriseRoots === 1) + ) { + this._status = Ci.nsIEnterprisePolicies.INACTIVE; + return; + } + + this._activatePolicies(); }, _reportEnterpriseTelemetry() { @@ -148,45 +183,72 @@ EnterprisePoliciesManager.prototype = { Glean.policies.isEnterprise.set(this.isEnterprise); }, - _chooseProvider(handler) { + _chooseProvider() { let platformProvider = null; - if (AppConstants.platform == "win" && AppConstants.MOZ_SYSTEM_POLICIES) { - platformProvider = new WindowsGPOPoliciesProvider(); - platformProvider.onPoliciesChanges(handler); - } else if ( - AppConstants.platform == "macosx" && - AppConstants.MOZ_SYSTEM_POLICIES - ) { - platformProvider = new macOSPoliciesProvider(); - platformProvider.onPoliciesChanges(handler); + if (AppConstants.MOZ_SYSTEM_POLICIES) { + if (AppConstants.platform == "win") { + platformProvider = new WindowsGPOPoliciesProvider(); + } else if (AppConstants.platform == "macosx") { + platformProvider = new macOSPoliciesProvider(); + } } - let jsonProvider = new JSONPoliciesProvider(); - jsonProvider.onPoliciesChanges(handler); - let remoteProvider = RemotePoliciesProvider.createInstance(); - remoteProvider.onPoliciesChanges(handler); if (platformProvider && platformProvider.hasPolicies) { if (jsonProvider.hasPolicies) { - return new CombinedProvider( - new CombinedProvider(remoteProvider, platformProvider), - jsonProvider - ); + return new CombinedProvider(platformProvider, jsonProvider); } - return new CombinedProvider(remoteProvider, platformProvider); + return platformProvider; } - if (jsonProvider.hasPolicies) { - return new CombinedProvider(remoteProvider, jsonProvider); + return jsonProvider; + }, + + /** + * Activates the policies that are provided during initialization. + */ + _activatePolicies() { + this._status = Ci.nsIEnterprisePolicies.ACTIVE; + + lazy.log.debug(this._provider); + + for (const [policyName, policyParams] of Object.entries( + this._provider.policies || {} + )) { + const { isValid, parsedParams } = this._validatePolicyParams( + policyName, + policyParams + ); + + if (!isValid) { + console.warn(`Parameters for policy ${policyName} are invalid`); + continue; + } + + this._parsedPolicies[policyName] = parsedParams; + + const policyImpl = lazy.Policies[policyName]; + this._scheduleActivationPolicyCallbacks(policyImpl, parsedParams); } - return remoteProvider; }, - _activatePolicies(unparsedPolicies) { - const { schema } = ChromeUtils.importESModule( - // eslint-disable-next-line mozilla/no-browser-refs-in-toolkit - "resource:///modules/policies/schema.sys.mjs" - ); + /** + * Parses, validates and applies any policy changes by comparing + * the previously parsed set of policies with the updated set + * from the remote provider. + * + * - Apply no changes to a policy if it remains unchanged. + * - Re-apply a policy if the parameters changed. + * - Remove a policy if it's missing in the updated set. + */ + _updatePolicies() { + if (this._status === Ci.nsIEnterprisePolicies.UNINITIALIZED) { + // Abort if we are still initializing or restarting the policy engine. + return; + } + + if (this._provider.isCombined) { + this._provider.mergePolicies(); + } - // Make a deep copy that will be trimmed later let previousPolicies = null; try { previousPolicies = structuredClone(this._parsedPolicies || {}); @@ -199,83 +261,82 @@ EnterprisePoliciesManager.prototype = { } } - this._parsedPolicies = {}; - - const policyNames = Object.keys(unparsedPolicies || {}); + this._schedulePolicyUpdates(previousPolicies); + this._schedulePolicyRemovals(previousPolicies); - for (let policyName of policyNames) { - let policySchema = schema.properties[policyName]; - let policyParameters = unparsedPolicies[policyName]; - - if (!policySchema) { - lazy.log.error(`Unknown policy: ${policyName}`); - continue; + for (const timing of Object.keys(this._callbacks)) { + if (timing !== "onRemove") { + const topic = this.topicByCallbackTiming[timing]; + if (!this._topicsObserved.has(topic)) { + // Only run callbacks for a timing that + // has already been observed. + continue; + } } + this._runPoliciesCallbacks(timing); + } + }, - let { valid: parametersAreValid, parsedValue: parsedParameters } = - lazy.JsonSchemaValidator.validate(policyParameters, policySchema, { - allowAdditionalProperties: true, - }); - - if (!parametersAreValid) { - lazy.log.error(`Invalid parameters specified for ${policyName}.`); - continue; - } + /** + * Parse and schedule a policy update + * + * @param {object} previousPolicies + */ + _schedulePolicyUpdates(previousPolicies) { + this._parsedPolicies = {}; - let policyImpl = lazy.Policies[policyName]; - if (!policyImpl) { - // This means there is an entry in the schema, but no implementaton. - // We only do this when we deprecate policies. - lazy.log.info(`${policyName} has been deprecated.`); - continue; - } + for (const [policyName, policyParams] of Object.entries( + this._provider.policies || {} + )) { + const { isValid, parsedParams } = this._validatePolicyParams( + policyName, + policyParams + ); - if (policyImpl.validate && !policyImpl.validate(parsedParameters)) { - lazy.log.error( - `Parameters for ${policyName} did not validate successfully.` - ); + if (!isValid) { continue; } - this._parsedPolicies[policyName] = parsedParameters; + this._parsedPolicies[policyName] = parsedParams; // verify the previous values if (policyName in previousPolicies) { const previousParameters = JSON.stringify(previousPolicies[policyName]); - if (previousParameters == JSON.stringify(parsedParameters)) { + if (previousParameters == JSON.stringify(parsedParams)) { + // Policy already active. No changes to policy needed. continue; } } - this._maybeCallbackPolicy(policyImpl, parsedParameters); + const policyImpl = lazy.Policies[policyName]; + this._scheduleActivationPolicyCallbacks(policyImpl, parsedParams); } + }, - // Only keep in this._previousPolicies the policies that are not part of the - // policies that were just received - this._previousPolicies = Object.fromEntries( - Object.keys(previousPolicies) - .filter(previousPolicyName => !policyNames.includes(previousPolicyName)) - .map(name => [name, previousPolicies[name]]) - ); - - const previousNames = Object.keys(this._previousPolicies).filter( - policyName => { - let policyImpl = lazy.Policies[policyName]; - if (!policyImpl) { - // This means there is an entry in the schema, but no implementaton. - // We only do this when we deprecate policies. - lazy.log.info(`${policyName} has been deprecated.`); - return false; - } - return true; + /** + * Schedule policy removals + * + * @param {object} previousPolicies + */ + _schedulePolicyRemovals(previousPolicies) { + // Schedule callbacks to remove policies that are no longer present + // in the latest set of parsed policies. + for (const [policyName, policyParams] of Object.entries(previousPolicies)) { + if (this._parsedPolicies[policyName] !== undefined) { + // Policy remains active. + continue; } - ); - for (let policyName of previousNames) { - let policyImpl = lazy.Policies[policyName]; + const policyImpl = lazy.Policies[policyName]; + if (!policyImpl) { + // This means there is an entry in the schema, but no implementation. + // We only do this when we deprecate policies. + lazy.log.warn(`The policy ${policyName} has been deprecated.`); + continue; + } - const onRemove = "onRemove" in policyImpl && policyImpl.onRemove; - if (!onRemove) { + if (!policyImpl.onRemove) { + lazy.log.warn(`Unable to remove the policy ${policyName}.`); continue; } @@ -283,15 +344,76 @@ EnterprisePoliciesManager.prototype = { policyImpl.onRemove, policyImpl, this /* the EnterprisePoliciesManager */, - this._previousPolicies[policyName], + policyParams, ]); } }, - // Schedule a policy callback if there is one to schedule - _maybeCallbackPolicy(policyImpl, parsedParameters = undefined) { + /** + * Validate and parse the policy parameters + * + * @param {object} policyName policy name + * @param {object} policyParams policy parameters + * @returns {{ isValid: boolean, parsedParams: object|null}} + */ + _validatePolicyParams(policyName, policyParams) { + const policySchema = this._policiesSchema.properties[policyName]; + + if (!policySchema) { + lazy.log.error(`Unknown policy: ${policyName}`); + return { isValid: false, parsedParams: null }; + } + + const { valid: isValid, parsedValue: parsedParams } = + lazy.JsonSchemaValidator.validate(policyParams, policySchema, { + allowAdditionalProperties: true, + }); + + if (!isValid) { + lazy.log.error(`Invalid parameters specified for ${policyName}.`); + return { isValid: false, parsedParams: null }; + } + + const policyImpl = lazy.Policies[policyName]; + if (!policyImpl) { + // This means there is an entry in the schema, but no implementaton. + // We only do this when we deprecate policies. + lazy.log.info(`${policyName} has been deprecated.`); + return { isValid: false, parsedParams: null }; + } + + if (policyImpl.validate && !policyImpl.validate(parsedParams)) { + lazy.log.error( + `Parameters for ${policyName} did not validate successfully.` + ); + return { isValid: false, parsedParams: null }; + } + + return { isValid, parsedParams }; + }, + + /** + * Policy implementation + * + * @typedef {object} PolicyImpl + * @property {Function} [onBeforeAddons] - callback that is invoked when notified of a policies-startup event + * @property {Function} [onProfileAfterChange] - callback that is invoked when notified of a profile-after-change event + * @property {Function} [onBeforeUIStartup] - callback that is invoked when notified of a final-ui-startup event + * @property {Function} [onAllWindowsRestored] - callback that is invoked when notified of a sessionstore-windows-restored event + * @property {Function} [onRemove] - callback that is invoked when a policy is explicitely removed + */ + + /** + * Schedule all "activating" callbacks, meaning any + * "onRemove" callbacks are skipped + * + * @param {PolicyImpl} policyImpl policy implementation + * @param {object} [parsedParams] parsed policy parameters + */ + _scheduleActivationPolicyCallbacks(policyImpl, parsedParams = undefined) { for (let timing of Object.keys(this._callbacks)) { if (timing === "onRemove") { + // Callbacks that remove policies are explicitely scheduled. continue; } @@ -301,7 +423,7 @@ EnterprisePoliciesManager.prototype = { policyCallback, policyImpl, this /* the EnterprisePoliciesManager */, - parsedParameters, + parsedParams, ]); } } @@ -331,7 +453,7 @@ EnterprisePoliciesManager.prototype = { onRemove: [], }, - _schedulePolicyCallback(timing, callback) { + _schedulePolicyCallback(timing, callbackArgs) { // Check for existence of the same callback. Since callback are .bind() // they cannot be just pushed to the array and checked for existence with // .includes() as each bind is a new different object. @@ -348,15 +470,15 @@ EnterprisePoliciesManager.prototype = { const exists = this._callbacks[timing].filter( e => - e[0] == callback[0] && - e[1] == callback[1] && - e[2] == callback[2] && - JSON.stringify(e[3]) == JSON.stringify(callback[3]) + e[0] == callbackArgs[0] && + e[1] == callbackArgs[1] && + e[2] == callbackArgs[2] && + JSON.stringify(e[3]) == JSON.stringify(callbackArgs[3]) ); if (exists.length) { return; } - this._callbacks[timing].push(callback); + this._callbacks[timing].push(callbackArgs); }, _runPoliciesCallbacks(timing) { @@ -373,17 +495,26 @@ EnterprisePoliciesManager.prototype = { } }, - async _restart() { + async _resetEngine() { DisallowedFeatures = {}; Services.ppmm.sharedData.delete("EnterprisePolicies:Status"); Services.ppmm.sharedData.delete("EnterprisePolicies:DisallowedFeatures"); this._status = Ci.nsIEnterprisePolicies.UNINITIALIZED; - this._parsedPolicies = undefined; + this._parsedPolicies = {}; + if (this.isRemotePoliciesSupported) { + RemotePoliciesProvider.dropInstance(); + } + this._provider = null; + this._topicsObserved = new Set(); for (let timing of Object.keys(this._callbacks)) { this._callbacks[timing] = []; } + }, + + async _restart() { + await this._resetEngine(); // 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. @@ -401,20 +532,20 @@ EnterprisePoliciesManager.prototype = { await notifyTopicOnIdle("distribution-customization-complete"); }, - observersReceived: [], + _topicsObserved: new Set(), - // nsIObserver implementation - observe: function BG_observe(subject, topic, data) { - const policiesCallbackMapping = { - onBeforeAddons: "policies-startup", - onProfileAfterChange: "profile-after-change", - onBeforeUIStartup: "final-ui-startup", - onAllWindowsRestored: "sessionstore-windows-restored", - }; + topicByCallbackTiming: { + onBeforeAddons: "policies-startup", + onProfileAfterChange: "profile-after-change", + onBeforeUIStartup: "final-ui-startup", + onAllWindowsRestored: "sessionstore-windows-restored", + }, - this.observersReceived.push(topic); + // nsIObserver implementation + observe(aSubject, aTopic) { + this._topicsObserved.add(aTopic); - switch (topic) { + switch (aTopic) { case "policies-startup": // Before the first set of policy callbacks runs, we must // initialize the service. @@ -435,26 +566,20 @@ EnterprisePoliciesManager.prototype = { this._runPoliciesCallbacks("onAllWindowsRestored"); break; + case "EnterprisePolicies:Reset": + this._resetEngine().then(null, console.error); + break; + case "EnterprisePolicies:Restart": this._restart().then(null, console.error); break; - case "EnterprisePolicies:Activate": { - const parsed = JSON.parse(data); - this._activatePolicies(parsed.policies); - - // Only run callbacks that are ready right now. The rest is handled by - // this._activatePolicies() - Object.keys(this._callbacks) - .filter( - cbName => - cbName !== "onRemove" && - this.observersReceived.includes(policiesCallbackMapping[cbName]) - ) - .map(cb => this._runPoliciesCallbacks(cb)); - - this._runPoliciesCallbacks("onRemove"); - + case "EnterprisePolicies:Update": { + this._updatePolicies(); + Services.obs.notifyObservers( + null, + "EnterprisePolicies:PolicyUpdatesApplied" + ); break; } @@ -641,6 +766,41 @@ EnterprisePoliciesManager.prototype = { return isEnterprise; }, + + /** + * Spin the event loop until the passed promise resolves. + * + * @param {Promise} promise + * @returns {any} Result of the resolved promise + */ + spinResolve(promise) { + if (!(promise instanceof Promise)) { + return promise; + } + let done = false; + let result = null; + let error = null; + promise + .catch(e => { + error = e; + }) + .then(r => { + result = r; + done = true; + }); + + Services.tm.spinEventLoopUntil( + "BrowserContentHandler.sys.mjs:BCH_spinResolve", + () => done + ); + if (!done) { + throw new Error("Forcefully exited event loop."); + } else if (error) { + throw error; + } else { + return result; + } + }, }; let DisallowedFeatures = {}; @@ -649,47 +809,46 @@ let ExtensionPolicies = null; let ExtensionSettings = null; let InstallSources = null; -// TODO: Those providers should likely inherit from a class to share some -// common parts. - -/* - * 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. +/** + * Basic policies provider */ - -class JSONPoliciesProvider { +class PoliciesProvider { constructor() { - this._changesHandlers = []; - this._policies = null; - this._readData(); - } - - onPoliciesChanges(handler) { - this._changesHandlers.push(handler); - if (this.hasPolicies) { - this.triggerOnPoliciesChanges(); - } + this._policies = {}; + this._failed = false; } - triggerOnPoliciesChanges() { - this._changesHandlers.forEach(callback => callback(this)); + get policies() { + return this._policies; } get hasPolicies() { return this._policies !== null && !isEmptyObject(this._policies); } - get policies() { - return this._policies; - } - get failed() { return this._failed; } + get isCombined() { + 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 extends PoliciesProvider { + constructor() { + super(); + this._readData(); + } + _getConfigurationFile() { let configFile = null; @@ -760,11 +919,14 @@ class JSONPoliciesProvider { if (data) { lazy.log.debug(`policies.json path = ${configFile.path}`); lazy.log.debug(`policies.json content = ${data}`); - this._policies = JSON.parse(data).policies; + const { policies } = JSON.parse(data); - if (!this._policies) { + if (!policies) { lazy.log.error("Policies file doesn't contain a 'policies' object"); + this._policies = {}; this._failed = true; + } else { + this._policies = policies; } } } catch (ex) { @@ -793,24 +955,32 @@ class JSONPoliciesProvider { * Uses JSON like JSONPoliciesProvider */ -class RemotePoliciesProvider { +class RemotePoliciesProvider extends PoliciesProvider { POLLING_FREQUENCY_PREF = "browser.policies.live_polling.frequency"; POLLING_FREQUENCY_FALLBACK = 60_000; POLLING_ENABLED_PREF = "browser.policies.live_polling.enabled"; static #instance = null; - static createInstance() { - if (!RemotePoliciesProvider.#instance) { - RemotePoliciesProvider.#instance = new RemotePoliciesProvider(); + static getInstance() { + if (!this.#instance) { + this.#instance = new this(); + } + return this.#instance; + } + + static dropInstance() { + if (!this.#instance) { + // No instance was initialized. + return; + } + if (this.#instance._poller) { + this.#instance._stopPolling(); } - return RemotePoliciesProvider.#instance; + this.#instance = null; } constructor() { - this._changesHandlers = []; - this._policies = null; - this._socket = null; - this._hasRemoteConnection = false; + super(); this._poller = null; this._pollingFrequency = Services.prefs.getIntPref( this.POLLING_FREQUENCY_PREF, @@ -823,25 +993,6 @@ class RemotePoliciesProvider { Services.prefs.addObserver(this.POLLING_FREQUENCY_PREF, this); Services.prefs.addObserver(this.POLLING_ENABLED_PREF, this); Services.obs.addObserver(this, "xpcom-shutdown"); - - this.init(); - } - - init() { - if (this._isPollingEnabled) { - this._startPolling(); - } - } - - onPoliciesChanges(handler) { - this._changesHandlers.push(handler); - if (this.hasPolicies) { - this.triggerOnPoliciesChanges(); - } - } - - triggerOnPoliciesChanges() { - this._changesHandlers.forEach(callback => callback(this)); } observe(aSubject, aTopic, aData) { @@ -858,7 +1009,7 @@ class RemotePoliciesProvider { return; } this._stopPolling(); - this._startPolling(); + this.startPolling(); } else if (aData === this.POLLING_ENABLED_PREF) { const p = this._isPollingEnabled; this._isPollingEnabled = Services.prefs.getBoolPref( @@ -869,7 +1020,7 @@ class RemotePoliciesProvider { return; } if (this._isPollingEnabled) { - this._startPolling(); + this.startPolling(); } else { this._stopPolling(); } @@ -886,46 +1037,26 @@ class RemotePoliciesProvider { } } - get hasRemoteConnection() { - return this._hasRemoteConnection; - } - - get hasPolicies() { - return this._policies !== null && !isEmptyObject(this._policies); - } - - get policies() { - return this._policies; - } - - get failed() { - return this._failed; - } - _stopPolling() { if (!this._poller) { return; } - this._hasRemoteConnection = false; lazy.clearInterval(this._poller); this._poller = null; } - _performPolling() { - lazy.ConsoleClient.getRemotePolicies() - .then(jsonResponse => { - this._hasRemoteConnection = true; - this._ingestPolicies(jsonResponse); - }) - .catch(error => { - console.warn( - `RemotePoliciesProvider performPolling() with frequency ${this._pollingFrequency} caused error ${error}` - ); - this._hasRemoteConnection = false; - }); + async _performPolling() { + try { + await this.ingestPolicies(); + Services.obs.notifyObservers(null, "EnterprisePolicies:Update"); + } catch (e) { + lazy.log.error( + `RemotePoliciesProvider performPolling() with frequency ${this._pollingFrequency} caused error ${e}` + ); + } } - _startPolling() { + startPolling() { if (!this._isPollingEnabled) { return; } @@ -936,31 +1067,27 @@ class RemotePoliciesProvider { ); } - _ingestPolicies(payload) { - if ("policies" in payload) { - this._policies = payload.policies; - this.triggerOnPoliciesChanges(); - Services.obs.notifyObservers( - null, - "EnterprisePolicies:Activate", - JSON.stringify(payload) - ); - } else { - // TODO, this is haha. meh. Maybe restart should be done by activate. + async ingestPolicies() { + if (!this._isPollingEnabled) { + return; + } + + const res = await lazy.ConsoleClient.getRemotePolicies(); + if (!res.policies) { this._policies = {}; - Services.obs.notifyObservers(null, "EnterprisePolicies:Restart"); - // Make sure that handler is triggered even when payload is empty as - // in "_cleanup" - this.triggerOnPoliciesChanges(); + console.error( + `Clearing remote policies because no policies were found in the response: ${JSON.stringify(res)}.` + ); + this._failed = true; + return; } + this._policies = res.policies; } } -class WindowsGPOPoliciesProvider { +class WindowsGPOPoliciesProvider extends PoliciesProvider { constructor() { - this._changesHandlers = []; - this._policies = null; - + super(); let wrk = Cc["@mozilla.org/windows-registry-key;1"].createInstance( Ci.nsIWindowsRegKey ); @@ -974,29 +1101,6 @@ class WindowsGPOPoliciesProvider { } } - onPoliciesChanges(handler) { - this._changesHandlers.push(handler); - if (this.hasPolicies) { - this.triggerOnPoliciesChanges(); - } - } - - triggerOnPoliciesChanges() { - this._changesHandlers.forEach(callback => callback(this.hasPolicies)); - } - - 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"; @@ -1026,81 +1130,45 @@ class WindowsGPOPoliciesProvider { } } -class macOSPoliciesProvider { +class macOSPoliciesProvider extends PoliciesProvider { constructor() { - this._changesHandlers = []; - this._policies = null; + super(); let prefReader = Cc["@mozilla.org/mac-preferences-reader;1"].createInstance( Ci.nsIMacPreferencesReader ); if (!prefReader.policiesEnabled()) { return; } - this._policies = lazy.macOSPoliciesParser.readPolicies(prefReader); - } - - onPoliciesChanges(handler) { - this._changesHandlers.push(handler); - if (this.hasPolicies) { - this.triggerOnPoliciesChanges(); - } - } - - triggerOnPoliciesChanges() { - this._changesHandlers.forEach(callback => callback(this.hasPolicies)); - } - - get hasPolicies() { - return this._policies !== null && Object.keys(this._policies).length; - } - - get policies() { - return this._policies; - } - - get failed() { - return this._failed; + this._policies = lazy.macOSPoliciesParser.readPolicies(prefReader) || {}; } } -class CombinedProvider { +class CombinedProvider extends PoliciesProvider { constructor(primaryProvider, secondaryProvider) { - this._readyProviders = 0; - this._primary = primaryProvider; - this._secondary = secondaryProvider; - this._primary.onPoliciesChanges(this.providerPoliciesChanged.bind(this)); - this._secondary.onPoliciesChanges(this.providerPoliciesChanged.bind(this)); - } - - providerPoliciesChanged() { - this._readyProviders++; - if (this._readyProviders === 2) { - this.combine(); - } + super(); + this._primaryProvider = primaryProvider; + this._secondaryProvider = secondaryProvider; + this.mergePolicies(); } - combine() { - // Combine policies with primary taking precedence. + mergePolicies() { + // Combine policies with primaryProvider taking precedence. // We only do this for top level policies. - this._policies = this._primary._policies; - for (let policyName of Object.keys(this._secondary.policies)) { + this._policies = structuredClone(this._primaryProvider.policies); + for (let [policyName, policyParams] of Object.entries( + this._secondaryProvider.policies || {} + )) { if (!(policyName in this._policies)) { - this._policies[policyName] = this._secondary.policies[policyName]; + this._policies[policyName] = policyParams; } } } - get hasPolicies() { - // Combined provider always has policies. - return true; - } - - get policies() { - return this._policies; + get failed() { + return this._primaryProvider.failed && this._secondaryProvider.failed; } - get failed() { - // Combined provider never fails. - return false; + get isCombined() { + return true; } } diff --git a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl index 62b1b568ce7e3..dba497fe2b670 100644 --- a/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl +++ b/toolkit/components/enterprisepolicies/nsIEnterprisePolicies.idl @@ -21,6 +21,13 @@ interface nsIEnterprisePolicies : nsISupports boolean isAllowed(in ACString feature); + /** + * Whether remote policies are supported (limited to enterprise builds) + * + * @return A boolean value defining is remote policies are supported. + */ + boolean isRemotePoliciesSupported(); + /** * Get the active policies that have been successfully parsed. * diff --git a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs index 3d3701e6b0fe5..292fb2185edb4 100644 --- a/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs +++ b/toolkit/components/enterprisepolicies/tests/EnterprisePolicyTesting.sys.mjs @@ -10,11 +10,54 @@ const lazy = {}; ChromeUtils.defineESModuleGetters(lazy, { FileTestUtils: "resource://testing-common/FileTestUtils.sys.mjs", modifySchemaForTests: "resource:///modules/policies/schema.sys.mjs", - HttpServer: "resource://testing-common/httpd.sys.mjs", - setTimeout: "resource://gre/modules/Timer.sys.mjs", + sinon: "resource://testing-common/Sinon.sys.mjs", + ConsoleClient: "resource:///modules/enterprise/ConsoleClient.sys.mjs", }); -export var EnterprisePolicyTesting = { +export const REMOTE_POLICIES_TESTING_PREF = "browser.policies.remote.enabled"; + +export const EnterprisePolicyTesting = { + /* The stub wrapping ConsoleClient.getRemotePolicies to control which remote policies are fetched */ + get remotePoliciesStub() { + return this._remotePoliciesStub; + }, + + set remotePoliciesStub(stub) { + this._remotePoliciesStub = stub; + }, + + /** + * Observe for all policies to be applied. This notification + * is sent when the policy engine is started up or reseted. + * + * @param {Promise} resolve Promise that resolves once all policies are applied. + */ + resolveOnceAllPoliciesApplied(resolve) { + Services.obs.addObserver(function observer() { + Services.obs.removeObserver( + observer, + "EnterprisePolicies:AllPoliciesApplied" + ); + resolve(); + }, "EnterprisePolicies:AllPoliciesApplied"); + }, + + /** + * Observe for a policy update. This notification is sent once + * we check the console for updated policies. + * + * @param {Promise} resolve Promise that resolves once the policy update is handled. + */ + resolveOnceAllPolicyUpdatesApplied(resolve) { + Services.obs.addObserver(function observer() { + Services.obs.removeObserver( + observer, + "EnterprisePolicies:PolicyUpdatesApplied" + ); + resolve(); + }, "EnterprisePolicies:PolicyUpdatesApplied"); + }, + // |json| must be an object representing the desired policy configuration, OR a // path to the JSON file containing the policy configuration. setupPolicyEngineWithJson: async function setupPolicyEngineWithJson( @@ -34,15 +77,8 @@ export var EnterprisePolicyTesting = { Services.prefs.setStringPref("browser.policies.alternatePath", filePath); - let promise = new Promise(resolve => { - Services.obs.addObserver(function observer() { - Services.obs.removeObserver( - observer, - "EnterprisePolicies:AllPoliciesApplied" - ); - resolve(); - }, "EnterprisePolicies:AllPoliciesApplied"); - }); + const { promise, resolve } = Promise.withResolvers(); + this.resolveOnceAllPoliciesApplied(resolve); // Clear any previously used custom schema or assign a new one lazy.modifySchemaForTests(customSchema || null); @@ -51,60 +87,61 @@ export var EnterprisePolicyTesting = { return promise; }, - servePolicyWithJson: async function servePolicyWithJson( - json, - customSchema, - registerCleanupFunction - ) { - if (this._httpd === undefined) { - this._httpd = new lazy.HttpServer(); - await this._httpd.start(-1); - const serverAddr = `http://localhost:${this._httpd.identity.primaryPort}`; - - const tokenData = { - access_token: "test_access_token", - refresh_token: "test_refresh_token", - expires_in: 3600, - token_type: "Bearer", - }; - - // Set up mock token endpoint for ConsoleClient (token refresh never hits it yet) - this._httpd.registerPathHandler("/sso/token", (req, resp) => { - resp.setStatusLine(req.httpVersion, 200, "OK"); - resp.setHeader("Content-Type", "application/json"); - resp.write(JSON.stringify(tokenData)); - }); - - Services.prefs.setStringPref("enterprise.console.address", serverAddr); - Services.prefs.setBoolPref("browser.policies.live_polling.enabled", true); - Services.felt.setTokens( - tokenData.access_token, - tokenData.refresh_token, - tokenData.expires_in - ); + awaitNextPolicyUpdate() { + const { promise, resolve } = Promise.withResolvers(); + this.resolveOnceAllPolicyUpdatesApplied(resolve); + return promise; + }, + + /** + * Apply the custom schema, setup the remote policies stub and + * trigger a restart of the policy engine. + * + * @param {object} policies + * @param {object} customSchema + * @returns {Promise} Promise that resolves once the set of policies are applied + */ + async servePolicyWithRemoteJson(policies, customSchema) { + lazy.modifySchemaForTests(customSchema || null); + + const policiesAppliedPromise = this.applyRemotePolicies(policies, false); + + Services.obs.notifyObservers(null, "EnterprisePolicies:Restart"); - registerCleanupFunction(async () => { - await new Promise(resolve => this._httpd.stop(resolve)); - this._httpd = undefined; - Services.prefs.clearUserPref("enterprise.console.address"); - Services.prefs.clearUserPref("browser.policies.live_polling.enabled"); - const { ConsoleClient } = ChromeUtils.importESModule( - "resource:///modules/enterprise/ConsoleClient.sys.mjs" - ); - ConsoleClient.clearTokenData(); - }); + return policiesAppliedPromise; + }, + + /** + * Listen for the policies to be applied and stub the remote policies. + * + * @param {object} policies + * @param {boolean} isUpdate Whether the promise resolves once all policies are + * applied on startup or once the policy update is complete + * @returns {Promise} Promise that resolves once the set of policies are applied + */ + async applyRemotePolicies(policies, isUpdate = true) { + const { promise, resolve } = Promise.withResolvers(); + if (isUpdate) { + // Resolve once policies are updated + this.resolveOnceAllPolicyUpdatesApplied(resolve); + } else { + // Resolve once all policies are applied on initial activation + this.resolveOnceAllPoliciesApplied(resolve); } - let { promise, resolve } = Promise.withResolvers(); + if (this.remotePoliciesStub) { + this.remotePoliciesStub.restore(); + } + this.remotePoliciesStub = lazy.sinon.stub( + lazy.ConsoleClient, + "getRemotePolicies" + ); + + const returnRemotePolicies = () => { + return Promise.resolve(policies); + }; - this._httpd.registerPathHandler("/api/browser/policies", (req, resp) => { - resp.setStatusLine(req.httpVersion, 200, "OK"); - resp.write(JSON.stringify(json)); - lazy.modifySchemaForTests(customSchema || null); - lazy.setTimeout(() => { - resolve(); - }, 100); - }); + this.remotePoliciesStub.callsFake(returnRemotePolicies); return promise; }, @@ -208,7 +245,9 @@ export var PoliciesPrefTracker = { // If a pref was used through setDefaultPref instead // of setAndLockPref, it wasn't locked, but calling // unlockPref is harmless - Preferences.unlock(prefName); + if (Services.prefs.prefIsLocked(prefName)) { + Preferences.unlock(prefName); + } if (stored.originalDefaultValue !== undefined) { defaults.set(prefName, stored.originalDefaultValue); diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser.toml b/toolkit/components/enterprisepolicies/tests/browser/browser.toml index 054db55d80de7..b680d961d6914 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser.toml +++ b/toolkit/components/enterprisepolicies/tests/browser/browser.toml @@ -2,28 +2,20 @@ head = "head.js" support-files = ["config_broken_json.json"] prefs = [ - "enterprise.console.address=", - "browser.policies.testUseHttp=false", - "browser.policies.live_polling.frequency=500", + "browser.policies.remote.enabled=false", +] +environment = [ + "MOZ_BYPASS_FELT=1", + "MOZ_AUTOMATION=1", ] ["browser_policies_basic_tests.js"] -["browser_policies_basiclive_tests.js"] - ["browser_policies_broken_json.js"] -["browser_policies_diffing.js"] - ["browser_policies_gpo.js"] run-if = [ "os == 'win'", ] -["browser_policies_live_proxy.js"] - -["browser_policies_missing_live.js"] - ["browser_policies_mistyped_json.js"] - -["browser_policies_prefs.js"] diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js index 8f4eec8716da0..513b38366eb84 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js +++ b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basic_tests.js @@ -3,10 +3,6 @@ "use strict"; -add_setup(function test_set_local_file_usage() { - SpecialPowers.pushPrefEnv({ set: [["browser.policies.testUseHttp", false]] }); -}); - add_task(test_simple_policies); add_task(async function test_policy_cleanup() { diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basiclive_tests.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basiclive_tests.js deleted file mode 100644 index 4b24dd9e9c1a7..0000000000000 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_basiclive_tests.js +++ /dev/null @@ -1,27 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -// TODO: We only test the RemotePoliciesProvider here, it would be nice -// to enhance with testing combinations of providers - -add_setup(async function test_set_http_server_usage() { - await SpecialPowers.pushPrefEnv({ - set: [["browser.policies.testUseHttp", true]], - }); - - await EnterprisePolicyTesting.servePolicyWithJson( - {}, - {}, - registerCleanupFunction - ); - assertOverHttp(); -}); - -add_task(test_simple_policies); - -add_task(async function test_policy_cleanup() { - await EnterprisePolicyTesting.servePolicyWithJson({}, {}); - assert_policy_cleanup(); -}); diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_diffing.js b/toolkit/components/enterprisepolicies/tests/browser/browser_policies_diffing.js deleted file mode 100644 index c258fc1f72759..0000000000000 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_diffing.js +++ /dev/null @@ -1,164 +0,0 @@ -/* Any copyright is dedicated to the Public Domain. - * http://creativecommons.org/publicdomain/zero/1.0/ */ - -"use strict"; - -add_setup(async function test_set_http_server_usage() { - await SpecialPowers.pushPrefEnv({ - set: [ - ["browser.policies.live_polling_freq", 250], - ["browser.policies.testUseHttp", true], - ], - }); - - await EnterprisePolicyTesting.servePolicyWithJson( - {}, - {}, - registerCleanupFunction - ); - - if (Services.prefs.getBoolPref("browser.policies.testUseHttp")) { - assertOverHttp(); - } -}); - -const lazy = {}; - -ChromeUtils.defineESModuleGetters(lazy, { - Policies: "resource:///modules/policies/Policies.sys.mjs", - setTimeout: "resource://gre/modules/Timer.sys.mjs", -}); - -add_task(async function test_simple_policy_removal() { - const customSchema = { - properties: { - BlockSomePage: { - type: "boolean", - }, - }, - }; - - let blockSomePageApplied = false; - - // Inspired by BlockAboutConfig - lazy.Policies.BlockSomePage = { - onBeforeUIStartup(manager, param) { - if (param) { - blockSomePageApplied = true; - } - }, - onRemove(manager, oldParam) { - if (oldParam) { - // Previous policy param was "true" so revert and disable the blocking - blockSomePageApplied = false; - } - }, - }; - - await setupPolicyEngineWithJson( - { - policies: { - BlockSomePage: true, - }, - }, - customSchema - ); - - ok(blockSomePageApplied, "BlockSomePage enabled"); - - await setupPolicyEngineWithJson( - { - policies: {}, - }, - customSchema - ); - - ok(!blockSomePageApplied, "BlockSomePage disabled"); - - delete lazy.Policies.BlockSomePage; -}); - -add_task(async function test_simple_policy_stays() { - const customSchema = { - properties: { - BlockAnotherPage: { - type: "boolean", - }, - }, - }; - - let blockAnotherPageApplied = false; - - // Inspired by BlockAboutConfig - lazy.Policies.BlockAnotherPage = { - onBeforeUIStartup(manager, param) { - info(`BlockAnotherPage.onBeforeUIStartup(${param})`); - if (param) { - blockAnotherPageApplied = true; - } - }, - onRemove(manager, oldParam) { - info(`BlockAnotherPage.onRemove(${oldParam})`); - if (oldParam) { - // Previous policy param was "true" so revert and disable the blocking - blockAnotherPageApplied = false; - } - }, - }; - - await setupPolicyEngineWithJson( - { - policies: { - BlockAnotherPage: true, - }, - }, - customSchema - ); - - // We received payload and applied once - ok(blockAnotherPageApplied, "BlockAnotherPage enabled"); - - // This is not really representative of how things can happen but rather to - // verify that the policy's callback was not called a second time. - // - // Intended behavior is: - // - poll - // + get policy1 with param X=Y - // + apply policy1 with callback onBeforeUIStartup - // - poll - // + get policy1 with param X=Y - // + no change to policy1 so no call to onBeforeUIStartup - // + no state changed - // - // => This is where check happens because we locally changed the state, so - // it is expected that the state stays this way (and is technically - // incorrect WRT policy at the moment) - // - - blockAnotherPageApplied = false; - - // polling happens on a specific frequency so wait enough to be certain - await new Promise(resolve => lazy.setTimeout(resolve, 500)); - - ok(!blockAnotherPageApplied, "BlockAnotherPage not re-enabled by policy"); - - // Set back the correct value - blockAnotherPageApplied = true; - - // Now publish a new instance where the policy has been removed - await setupPolicyEngineWithJson( - { - policies: {}, - }, - customSchema - ); - - // Policy being removed it means the blocking should get lifted - ok(!blockAnotherPageApplied, "BlockAnotherPage disabled"); - - delete lazy.Policies.BlockAnotherPage; -}); - -add_task(async function policy_cleanup() { - await EnterprisePolicyTesting.servePolicyWithJson({}, {}); -}); diff --git a/toolkit/components/enterprisepolicies/tests/browser/head.js b/toolkit/components/enterprisepolicies/tests/browser/head.js index defbbbd718a48..e64e2b7f340ea 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/head.js +++ b/toolkit/components/enterprisepolicies/tests/browser/head.js @@ -4,10 +4,17 @@ "use strict"; -const { EnterprisePolicyTesting, PoliciesPrefTracker } = - ChromeUtils.importESModule( - "resource://testing-common/EnterprisePolicyTesting.sys.mjs" - ); +const { + EnterprisePolicyTesting, + PoliciesPrefTracker, + REMOTE_POLICIES_TESTING_PREF, +} = ChromeUtils.importESModule( + "resource://testing-common/EnterprisePolicyTesting.sys.mjs" +); + +const { Policies } = ChromeUtils.importESModule( + "resource:///modules/policies/Policies.sys.mjs" +); PoliciesPrefTracker.start(); registerCleanupFunction(() => { @@ -16,11 +23,14 @@ registerCleanupFunction(() => { async function setupPolicyEngineWithJson(json, customSchema) { PoliciesPrefTracker.restoreDefaultValues(); - const useHttp = Services.prefs.getBoolPref("browser.policies.testUseHttp"); - if (!useHttp) { - return setupPolicyWithJsonFile(json, customSchema); + const isRemotePoliciesTesting = Services.prefs.getBoolPref( + REMOTE_POLICIES_TESTING_PREF, + false + ); + if (isRemotePoliciesTesting) { + return servePolicyWithRemoteJson(json, customSchema); } - return servePolicyWithJson(json, customSchema, registerCleanupFunction); + return setupPolicyWithJsonFile(json, customSchema); } async function setupPolicyWithJsonFile(json, customSchema) { @@ -34,22 +44,14 @@ async function setupPolicyWithJsonFile(json, customSchema) { return EnterprisePolicyTesting.setupPolicyEngineWithJson(json, customSchema); } -function assertOverHttp() { - Assert.notEqual( - EnterprisePolicyTesting._httpd, - undefined, - "Making sure HTTP delivery" - ); -} - -async function servePolicyWithJson(json, customSchema) { - return EnterprisePolicyTesting.servePolicyWithJson(json, customSchema); +async function servePolicyWithRemoteJson(json, customSchema) { + return EnterprisePolicyTesting.servePolicyWithRemoteJson(json, customSchema); } function assert_policy_cleanup() { is( - Services.policies.getActivePolicies(), - undefined, + JSON.stringify(Services.policies.getActivePolicies()), + JSON.stringify({}), "No policies should be defined" ); is( @@ -60,10 +62,6 @@ function assert_policy_cleanup() { } async function test_simple_policies() { - let { Policies } = ChromeUtils.importESModule( - "resource:///modules/policies/Policies.sys.mjs" - ); - let policy0Ran = false, policy1Ran = false, policy2Ran = false, diff --git a/toolkit/components/enterprisepolicies/tests/browser/remote/browser.toml b/toolkit/components/enterprisepolicies/tests/browser/remote/browser.toml new file mode 100644 index 0000000000000..7fd6c63b955fe --- /dev/null +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser.toml @@ -0,0 +1,24 @@ +[DEFAULT] +head = "head.js" +support-files = ["../head.js"] +prefs = [ + "browser.policies.remote.enabled=true", + "browser.policies.live_polling.enabled=true", + "browser.policies.live_polling.frequency=500", +] +environment = [ + "MOZ_BYPASS_FELT=1", + "MOZ_AUTOMATION=1", +] + +["browser_remote_and_local_policies_merging.js"] + +["browser_remote_policies_basic.js"] + +["browser_remote_policies_diffing.js"] + +["browser_remote_policies_live_proxy.js"] + +["browser_remote_policies_missing_live.js"] + +["browser_remote_policies_prefs.js"] diff --git a/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_and_local_policies_merging.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_and_local_policies_merging.js new file mode 100644 index 0000000000000..f9f5aa39c66e4 --- /dev/null +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_and_local_policies_merging.js @@ -0,0 +1,152 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const customSchema = { + properties: { + simple_policy0: { + type: "string", + }, + + simple_policy1: { + type: "string", + }, + }, +}; + +let policy_value0 = POLICY_PARAM_STATE.DEFAULT; +let policy_value1 = POLICY_PARAM_STATE.DEFAULT; + +const simple_policy0 = { + onBeforeUIStartup: (_manager, param) => { + policy_value0 = param; + }, + onRemove: (_manager, _oldParam) => { + policy_value0 = POLICY_PARAM_STATE.REMOVED; + }, +}; + +const simple_policy1 = { + onBeforeUIStartup: (_manager, param) => { + policy_value1 = param; + }, + onRemove: (_manager, _oldParam) => { + policy_value1 = POLICY_PARAM_STATE.REMOVED; + }, +}; + +add_setup(() => { + Policies.simple_policy0 = simple_policy0; + Policies.simple_policy1 = simple_policy1; + + registerCleanupFunction(async () => { + delete Policies.simple_policy0; + delete Policies.simple_policy1; + await EnterprisePolicyTesting.setupPolicyEngineWithJson(""); + assert_policy_cleanup(); + }); +}); + +add_task(async function test_remote_policy_overrides_local_policy() { + policy_value0 = POLICY_PARAM_STATE.DEFAULT; + + const localPolicies = { + policies: { + simple_policy0: POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + }, + }; + let remotePolicies = { + policies: {}, + }; + + await setupPolicyEngineWithCombinedPolicyProvider( + localPolicies, + remotePolicies, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { simple_policy0: POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY }, + "Expected local policy simple_policy0 to be set." + ); + Assert.equal( + policy_value0, + POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + "Expected local policy simple_policy0 to be set." + ); + + remotePolicies = { + policies: { + simple_policy0: POLICY_PARAM_STATE.UPDATED, + }, + }; + + await EnterprisePolicyTesting.applyRemotePolicies(remotePolicies); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { simple_policy0: POLICY_PARAM_STATE.UPDATED }, + "Expected remote policy update to override local policies." + ); + Assert.equal( + policy_value0, + POLICY_PARAM_STATE.UPDATED, + "Expected remote policy update to override local policies." + ); + + // Remove remote policies, re-apply local policy + await EnterprisePolicyTesting.applyRemotePolicies({ policies: {} }); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { simple_policy0: POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY }, + "Expected local policy to be re-applied if the remote policy is removed." + ); + Assert.equal( + policy_value0, + POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + "Expected local policy to be re-applied if the remote policy is removed." + ); +}); + +add_task(async function test_remote_and_local_policy_merged() { + policy_value1 = POLICY_PARAM_STATE.DEFAULT; + + const localPolicies = { + policies: { + simple_policy0: POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + }, + }; + let remotePolicies = { + policies: { + simple_policy1: POLICY_PARAM_STATE.APPLIED_REMOTE_POLICY, + }, + }; + + await setupPolicyEngineWithCombinedPolicyProvider( + localPolicies, + remotePolicies, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { + simple_policy0: POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + simple_policy1: POLICY_PARAM_STATE.APPLIED_REMOTE_POLICY, + }, + "Expected local and remote policy to be merged." + ); + Assert.equal( + policy_value0, + POLICY_PARAM_STATE.APPLIED_LOCAL_POLICY, + "Expected local policy to be applied." + ); + Assert.equal( + policy_value1, + POLICY_PARAM_STATE.APPLIED_REMOTE_POLICY, + "Expected local policy to be applied." + ); +}); diff --git a/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_basic.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_basic.js new file mode 100644 index 0000000000000..9c3a8176ce58b --- /dev/null +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_basic.js @@ -0,0 +1,8 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../head.js */ + +"use strict"; + +add_task(test_simple_policies); diff --git a/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_diffing.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_diffing.js new file mode 100644 index 0000000000000..0bd26b2a10c3c --- /dev/null +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_diffing.js @@ -0,0 +1,209 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const customSchema = { + properties: { + TestPolicy: { + type: "string", + }, + }, +}; + +let policyValue = POLICY_PARAM_STATE.DEFAULT; + +const TestPolicy = { + onBeforeUIStartup(manager, param) { + policyValue = param; + }, + onRemove(_manager, _oldParam) { + policyValue = POLICY_PARAM_STATE.REMOVED; + }, +}; + +add_setup(async () => { + Policies.TestPolicy = TestPolicy; + + registerCleanupFunction(async () => { + delete Policies.TestPolicy; + }); +}); + +add_task(async function test_policy_update_apply_new_policy() { + policyValue = POLICY_PARAM_STATE.DEFAULT; + + await setupPolicyEngineWithJson( + { + policies: {}, + }, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + {}, + "Expected no policies to be applied." + ); + Assert.equal( + policyValue, + POLICY_PARAM_STATE.DEFAULT, + "Expected the default policy parameter." + ); + + const policies = { + policies: { + TestPolicy: POLICY_PARAM_STATE.APPLIED, + }, + }; + + await EnterprisePolicyTesting.applyRemotePolicies(policies); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.APPLIED }, + "Expected remote policy TestPolicy with parameter APPLIED." + ); +}); + +add_task(async function test_policy_update_apply_policy_param_update() { + policyValue = POLICY_PARAM_STATE.DEFAULT; + + await setupPolicyEngineWithJson( + { + policies: { + TestPolicy: POLICY_PARAM_STATE.APPLIED, + }, + }, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.APPLIED }, + "Expected remote policy TestPolicy with parameter APPLIED." + ); + + policyValue = POLICY_PARAM_STATE.DEFAULT; + + const policies = { + policies: { + TestPolicy: POLICY_PARAM_STATE.UPDATED, + }, + }; + + await EnterprisePolicyTesting.applyRemotePolicies(policies); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.UPDATED }, + "Expected remote policy TestPolicy with parameter UPDATED." + ); +}); + +add_task(async function test_policy_update_remove_old_policy() { + policyValue = POLICY_PARAM_STATE.DEFAULT; + + await setupPolicyEngineWithJson( + { + policies: { + TestPolicy: POLICY_PARAM_STATE.APPLIED, + }, + }, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.APPLIED }, + "Expected remote policy TestPolicy with parameter APPLIED." + ); + + const policies = { + policies: {}, + }; + + await EnterprisePolicyTesting.applyRemotePolicies(policies); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + {}, + "Expected remote policy TestPolicy to be removed." + ); + Assert.equal( + policyValue, + POLICY_PARAM_STATE.REMOVED, + "Expected the policy parameter to be of state REMOVED." + ); +}); + +add_task(async function test_policy_update_no_changes() { + policyValue = POLICY_PARAM_STATE.DEFAULT; + + await setupPolicyEngineWithJson( + { + policies: { + TestPolicy: POLICY_PARAM_STATE.APPLIED, + }, + }, + customSchema + ); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.APPLIED }, + "Expected remote policy TestPolicy with parameter APPLIED." + ); + + // This is not really representative of how things can happen but rather to + // verify that the policy's callback was not called a second time. + // + // Intended behavior is: + // - poll + // + get policy1 with param X=Y + // + apply policy1 with callback onBeforeUIStartup + // - poll + // + get policy1 with param X=Y + // + no change to policy1 so no call to onBeforeUIStartup + // + no state changed + // + // => This is where check happens because we locally changed the state, so + // it is expected that the state stays this way (and is technically + // incorrect WRT policy at the moment) + // + + // Revert back to DEFAULT + policyValue = POLICY_PARAM_STATE.DEFAULT; + + // Wait for next policy update to complete + await EnterprisePolicyTesting.awaitNextPolicyUpdate(); + + // Verify that the policy's callback wasn't called a second time. + Assert.deepEqual( + Services.policies.getActivePolicies(), + { TestPolicy: POLICY_PARAM_STATE.APPLIED }, + "Expected no changes to the active policy specifications." + ); + Assert.equal( + policyValue, + POLICY_PARAM_STATE.DEFAULT, + "Expected local changes to policy parameters to not get overridden." + ); + + const policies = { + policies: {}, + }; + + await EnterprisePolicyTesting.applyRemotePolicies(policies); + + Assert.deepEqual( + Services.policies.getActivePolicies(), + {}, + "Expected remote policy TestPolicy to be removed." + ); + Assert.equal( + policyValue, + POLICY_PARAM_STATE.REMOVED, + "Expected the policy parameter to be of state REMOVED." + ); +}); diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_live_proxy.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_live_proxy.js similarity index 90% rename from toolkit/components/enterprisepolicies/tests/browser/browser_policies_live_proxy.js rename to toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_live_proxy.js index b346e00857a31..56eb6c18e3448 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_live_proxy.js +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_live_proxy.js @@ -43,20 +43,6 @@ function checkProxyPref(proxytype, address, port, unlocked = true) { } } -add_setup(async function test_set_http_server_usage() { - await SpecialPowers.pushPrefEnv({ - set: [["browser.policies.testUseHttp", true]], - }); - - await EnterprisePolicyTesting.servePolicyWithJson( - {}, - {}, - registerCleanupFunction - ); - - assertOverHttp(); -}); - add_task(async function test_apply_then_remove_proxy() { // Assert proxy settings are not set checkProxyPref("http", "", 0); @@ -70,6 +56,7 @@ add_task(async function test_apply_then_remove_proxy() { "changeProxySettings is allowed" ); + info("Setting up policy engine.") await setupPolicyEngineWithJson( { policies: { @@ -96,12 +83,11 @@ add_task(async function test_apply_then_remove_proxy() { "changeProxySettings is blocked" ); - // New policy removing proxy - await setupPolicyEngineWithJson( + // Remove Proxy policy + await EnterprisePolicyTesting.applyRemotePolicies( { policies: {}, }, - null ); // Assert proxy settings are remove @@ -157,12 +143,11 @@ add_task(async function test_apply_then_remove_proxy_locked() { "changeProxySettings is blocked" ); - // New policy removing proxy - await setupPolicyEngineWithJson( + // Remove Proxy policy + await EnterprisePolicyTesting.applyRemotePolicies( { policies: {}, }, - null ); // Assert proxy settings are remove @@ -206,7 +191,7 @@ add_task(async function test_apply_proxy_then_change_proxy() { ); // Network change from device posture? New policy - await setupPolicyEngineWithJson( + await EnterprisePolicyTesting.applyRemotePolicies( { policies: { Proxy: { @@ -217,7 +202,6 @@ add_task(async function test_apply_proxy_then_change_proxy() { }, }, }, - null ); // Assert proxy settings are set @@ -231,8 +215,4 @@ add_task(async function test_apply_proxy_then_change_proxy() { true, "changeProxySettings is allowed" ); -}); - -add_task(async function policy_cleanup() { - await EnterprisePolicyTesting.servePolicyWithJson({}, {}); -}); +}); \ No newline at end of file diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_missing_live.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_missing_live.js similarity index 79% rename from toolkit/components/enterprisepolicies/tests/browser/browser_policies_missing_live.js rename to toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_missing_live.js index d9bacd2285426..5b935a1fb4709 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_missing_live.js +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_missing_live.js @@ -10,7 +10,7 @@ ChromeUtils.defineESModuleGetters(lazy, { }); add_task(async function check_all_policies_are_live() { - const allPolicies = new Set(Object.keys(lazy.Policies)); + const policies = new Set(Object.keys(lazy.Policies)); // Set of policies we know cannot be live const notLivePolicies = new Set([ @@ -108,44 +108,33 @@ add_task(async function check_all_policies_are_live() { "WindowsSSO", ]); - const allLivePolicies = allPolicies.difference(notLivePolicies); + const livePolicies = policies.difference(notLivePolicies); - let liveEnabled = new Set(); + let policiesAppliable = new Set(); - for (let policyName of allPolicies) { + for (const policyName of policies) { const policy = lazy.Policies[policyName]; const hasOnRemove = typeof policy.onRemove === "function"; if (hasOnRemove) { - liveEnabled.add(policyName); + policiesAppliable.add(policyName); } } - const notEnabled = [ - ...allLivePolicies - .difference(liveEnabled) - .entries() - .map(e => e[0]), - ]; - if (notEnabled.length) { - console.debug(`Not enabled live policies`, JSON.stringify(notEnabled)); + const livePoliciesNotAppliable = livePolicies.difference(policiesAppliable) + if (livePoliciesNotAppliable.size) { + console.debug(`Live policies that are not appliable because of missing remove functions ${JSON.stringify(livePoliciesNotAppliable)}`); } - Assert.equal(notEnabled.length, 0, "Not all policies are live. Work better."); + Assert.equal(livePoliciesNotAppliable.size, 0, "Not all policies are live. Work better."); - const liveAndNotLive = [ - ...liveEnabled - .intersection(notLivePolicies) - .entries() - .map(e => e[0]), - ]; - if (liveAndNotLive.length) { + const liveAndNotLive = policiesAppliable.intersection(notLivePolicies) + if (liveAndNotLive.size) { console.debug( - `Inconsistent state: live and not live`, - JSON.stringify(liveAndNotLive) + `Inconsistent state: live and not live ${JSON.stringify(liveAndNotLive)}` ); } Assert.equal( - liveAndNotLive.length, + liveAndNotLive.size, 0, "There should be no policy both live and not live." ); diff --git a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_prefs.js b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_prefs.js similarity index 55% rename from toolkit/components/enterprisepolicies/tests/browser/browser_policies_prefs.js rename to toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_prefs.js index ed0c920529c89..25701c3db1e92 100644 --- a/toolkit/components/enterprisepolicies/tests/browser/browser_policies_prefs.js +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/browser_remote_policies_prefs.js @@ -3,89 +3,54 @@ "use strict"; -const lazy = {}; +const { setAndLockPref, unsetAndUnlockPref } = ChromeUtils.importESModule( + "resource:///modules/policies/Policies.sys.mjs" +); -ChromeUtils.defineESModuleGetters(lazy, { - Policies: "resource:///modules/policies/Policies.sys.mjs", - setAndLockPref: "resource:///modules/policies/Policies.sys.mjs", - unsetAndUnlockPref: "resource:///modules/policies/Policies.sys.mjs", -}); - -add_setup(async function test_set_http_server_usage() { - await SpecialPowers.pushPrefEnv({ - set: [["browser.policies.testUseHttp", true]], - }); - - await EnterprisePolicyTesting.servePolicyWithJson( - {}, - {}, - registerCleanupFunction - ); - - if (Services.prefs.getBoolPref("browser.policies.testUseHttp")) { - assertOverHttp(); - } -}); +const PREF_VALUE = { + DEFAULT: "default", + USER_CHANGED: "user-changed", + POLICY_DEFAULT: "policy-default", + POLICY_CHANGED: "policy-changed", +} add_task(async function test_simple_policy_pref_setAndLock() { - const customSchema = { - properties: { - SetSomePref: { - type: "boolean", - }, - }, - }; - + const prefName = "browser.tests.some_random_pref"; - // just something random - const prefValue = "fcf57517-a524-4468-bff7-0817b2ad6a31"; try { Services.prefs.getStringPref(prefName); ok(false, `Pref ${prefName} exists, this should not happen`); } catch { - ok(true, `Pref ${prefName} does not exists`); + ok(true, `Pref ${prefName} does not exist`); } - lazy.Policies.SetSomePref = { - onBeforeUIStartup(manager, param) { - if (param) { - lazy.setAndLockPref(prefName, `${prefValue}-policyDefault`); - } - }, - onRemove(manager, oldParams) { - if (oldParams) { - lazy.unsetAndUnlockPref(prefName, `${prefValue}-policyDefault`); - } - }, - }; - let defaults = Services.prefs.getDefaultBranch(""); - defaults.setStringPref(prefName, prefValue); + defaults.setStringPref(prefName, PREF_VALUE.DEFAULT); // Assert default pref value is( Services.prefs.getStringPref(prefName), - prefValue, - "default pref value returned via Services.prefs." + PREF_VALUE.DEFAULT, + "Correct default pref value returned via Services.prefs." ); is( defaults.getStringPref(prefName), - prefValue, - "default pref value returned via defaults." + PREF_VALUE.DEFAULT, + "Correct default pref value returned via defaults." ); - Services.prefs.setStringPref(prefName, `${prefValue}-user`); + Services.prefs.setStringPref(prefName, PREF_VALUE.USER_CHANGED); // Assert user value works is( Services.prefs.getStringPref(prefName), - `${prefValue}-user`, + PREF_VALUE.USER_CHANGED, "user pref value returned via Services.prefs." ); is( defaults.getStringPref(prefName), - prefValue, + PREF_VALUE.DEFAULT, "default pref value returned via defaults." ); @@ -96,10 +61,31 @@ add_task(async function test_simple_policy_pref_setAndLock() { "Pref reports as not locked" ); + const customSchema = { + properties: { + SetSomePref: { + type: "string", + }, + }, + }; + + Policies.SetSomePref = { + onBeforeUIStartup(manager, param) { + if (param) { + setAndLockPref(prefName, param); + } + }, + onRemove(manager, oldParams) { + if (oldParams) { + unsetAndUnlockPref(prefName); + } + }, + }; + await setupPolicyEngineWithJson( { policies: { - SetSomePref: true, + SetSomePref: PREF_VALUE.POLICY_DEFAULT, }, }, customSchema @@ -108,41 +94,60 @@ add_task(async function test_simple_policy_pref_setAndLock() { // Assert pref value set and locked, default value returned is( Services.prefs.getStringPref(prefName), - `${prefValue}-policyDefault`, + PREF_VALUE.POLICY_DEFAULT, "new default pref value returned via Services.prefs." ); is( defaults.getStringPref(prefName), - `${prefValue}-policyDefault`, + PREF_VALUE.POLICY_DEFAULT, "new default pref value returned via defaults." ); is(true, Services.prefs.prefIsLocked(prefName), "Pref reports as locked"); - await setupPolicyEngineWithJson( + await EnterprisePolicyTesting.applyRemotePolicies( + { + policies: { + SetSomePref: PREF_VALUE.POLICY_CHANGED, + }, + }, + ); + + // Assert pref value set and locked, default value returned + is( + Services.prefs.getStringPref(prefName), + PREF_VALUE.POLICY_CHANGED, + "new changed pref value returned via Services.prefs." + ); + is( + defaults.getStringPref(prefName), + PREF_VALUE.POLICY_CHANGED, + "new changed pref value returned via defaults." + ); + is(true, Services.prefs.prefIsLocked(prefName), "Pref remains as locked"); + + await EnterprisePolicyTesting.applyRemotePolicies( { policies: {}, }, - customSchema ); // Assert original default pref and user value returned again is( Services.prefs.getStringPref(prefName), - `${prefValue}-user`, + PREF_VALUE.USER_CHANGED, "original user pref value returned via Services.prefs." ); is( defaults.getStringPref(prefName), - prefValue, + PREF_VALUE.DEFAULT, "original default pref value returned via defaults." ); - is(false, Services.prefs.prefIsLocked(prefName), "Pref reports as locked"); - delete lazy.Policies.SetSomePref; + is(false, Services.prefs.prefIsLocked(prefName), "Pref reports as unlocked"); + + delete Policies.SetSomePref; Services.prefs.deleteBranch(prefName); }); -add_task(async function policy_cleanup() { - await EnterprisePolicyTesting.servePolicyWithJson({}, {}); -}); + \ No newline at end of file diff --git a/toolkit/components/enterprisepolicies/tests/browser/remote/head.js b/toolkit/components/enterprisepolicies/tests/browser/remote/head.js new file mode 100644 index 0000000000000..f0d3b39939067 --- /dev/null +++ b/toolkit/components/enterprisepolicies/tests/browser/remote/head.js @@ -0,0 +1,63 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* import-globals-from ../head.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/toolkit/components/enterprisepolicies/tests/browser/head.js", + this +); + +const POLICY_PARAM_STATE = { + DEFAULT: "default", + APPLIED: "applied", + APPLIED_LOCAL_POLICY: "applied-by-local-policy", + APPLIED_REMOTE_POLICY: "applied-by-remote-policy", + UPDATED: "updated", + REMOVED: "removed", +}; + +add_setup(async () => { + registerCleanupFunction(async () => { + Services.obs.notifyObservers(null, "EnterprisePolicies:Reset"); + if (EnterprisePolicyTesting.remotePoliciesStub) { + EnterprisePolicyTesting.remotePoliciesStub.restore(); + EnterprisePolicyTesting.remotePoliciesStub = null; + } + }); +}); + +/** + * Set up a policy engine that combined local and regularly fetched remote policies. + * + * @param {object} localPolicies Policies to be read from a local policies.json + * @param {object} remotePolicies Policies to be fetched from a stubed ConsoleClient endpoint + * @param {object} customSchema + * + * @returns {Promise} Promise that resolves once local and remote policies are applied after a policy engine restart. + */ +async function setupPolicyEngineWithCombinedPolicyProvider( + localPolicies, + remotePolicies, + customSchema +) { + PoliciesPrefTracker.restoreDefaultValues(); + + // Stub remote policies endpoint + const remotePoliciesAppliedPromise = + EnterprisePolicyTesting.applyRemotePolicies(remotePolicies, false); + + // Put local policies in place (local policies.json file) + const localPoliciesAppliedPromise = setupPolicyWithJsonFile( + localPolicies, + customSchema + ); + + // Waiting for the "EnterprisePolicies:PolicyUpdatesApplied" notification + return Promise.all([ + localPoliciesAppliedPromise, + remotePoliciesAppliedPromise, + ]); +} diff --git a/toolkit/components/enterprisepolicies/tests/moz.build b/toolkit/components/enterprisepolicies/tests/moz.build index 14d1ec3803619..6bad3c01cf2b5 100644 --- a/toolkit/components/enterprisepolicies/tests/moz.build +++ b/toolkit/components/enterprisepolicies/tests/moz.build @@ -8,6 +8,11 @@ BROWSER_CHROME_MANIFESTS += [ "browser/browser.toml", ] +if CONFIG["MOZ_ENTERPRISE"]: + BROWSER_CHROME_MANIFESTS += [ + "browser/remote/browser.toml", + ] + TESTING_JS_MODULES += [ "EnterprisePolicyTesting.sys.mjs", ] diff --git a/toolkit/modules/Preferences.sys.mjs b/toolkit/modules/Preferences.sys.mjs index 91b68b5bfbfee..d5bd7e86acf85 100644 --- a/toolkit/modules/Preferences.sys.mjs +++ b/toolkit/modules/Preferences.sys.mjs @@ -246,7 +246,7 @@ Preferences.lock = function (prefName) { * Unlock a pref so it can be changed. * * @param prefName {String|Array} - * the pref to lock, or an array of prefs to lock + * the pref to unlock, or an array of prefs to unlock */ Preferences.unlock = function (prefName) { if (Array.isArray(prefName)) {