diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 95c1befbc58a..7eec89789cc9 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1935,6 +1935,7 @@ pref("signon.management.page.breachAlertUrl", "https://monitor.firefox.com/breach-details/"); pref("signon.management.page.showPasswordSyncNotification", true); pref("signon.passwordEditCapture.enabled", true); +pref("signon.relatedRealms.enabled", false); pref("signon.showAutoCompleteFooter", true); pref("signon.showAutoCompleteImport", "import"); pref("signon.suggestImportCount", 3); diff --git a/modules/libpref/init/all.js b/modules/libpref/init/all.js index c9122583ce9e..c984a8b0037c 100644 --- a/modules/libpref/init/all.js +++ b/modules/libpref/init/all.js @@ -3675,6 +3675,7 @@ pref("signon.userInputRequiredToCapture.enabled", true); pref("signon.debug", false); pref("signon.recipes.path", "resource://app/defaults/settings/main/password-recipes.json"); pref("signon.recipes.remoteRecipesEnabled", true); +pref("signon.relatedRealms.enabled", false); pref("signon.schemeUpgrades", true); pref("signon.includeOtherSubdomainsInLookup", true); diff --git a/toolkit/components/passwordmgr/LoginHelper.jsm b/toolkit/components/passwordmgr/LoginHelper.jsm index 452b681f8f3f..fc1689be2228 100644 --- a/toolkit/components/passwordmgr/LoginHelper.jsm +++ b/toolkit/components/passwordmgr/LoginHelper.jsm @@ -371,6 +371,8 @@ this.LoginHelper = { privateBrowsingCaptureEnabled: null, remoteRecipesEnabled: null, remoteRecipesCollection: "password-recipes", + relatedRealmsEnabled: null, + relatedRealmsCollection: "websites-with-shared-credential-backends", schemeUpgrades: null, showAutoCompleteFooter: null, showAutoCompleteImport: null, @@ -470,6 +472,9 @@ this.LoginHelper = { this.remoteRecipesEnabled = Services.prefs.getBoolPref( "signon.recipes.remoteRecipesEnabled" ); + this.relatedRealmsEnabled = Services.prefs.getBoolPref( + "signon.relatedRealms.enabled" + ); }, createLogger(aLogPrefix) { @@ -682,6 +687,8 @@ this.LoginHelper = { schemeUpgrades: false, acceptWildcardMatch: false, acceptDifferentSubdomains: false, + acceptRelatedRealms: false, + relatedRealms: [], } ) { if (aLoginOrigin == aSearchOrigin) { @@ -718,6 +725,18 @@ this.LoginHelper = { ) { return true; } + if ( + aOptions.acceptRelatedRealms && + aOptions.relatedRealms.length && + (loginURI.scheme == searchURI.scheme || + (aOptions.schemeUpgrades && schemeMatches)) + ) { + for (let relatedOrigin of aOptions.relatedRealms) { + if (Services.eTLD.hasRootDomain(loginURI.host, relatedOrigin)) { + return true; + } + } + } } if ( diff --git a/toolkit/components/passwordmgr/LoginManager.jsm b/toolkit/components/passwordmgr/LoginManager.jsm index 3e75546a7a2b..f65bd26c7e9a 100644 --- a/toolkit/components/passwordmgr/LoginManager.jsm +++ b/toolkit/components/passwordmgr/LoginManager.jsm @@ -322,7 +322,6 @@ LoginManager.prototype = { if (matchingLogin) { throw LoginHelper.createLoginAlreadyExistsError(matchingLogin.guid); } - log.debug("Adding login"); return this._storage.addLogin(login); }, diff --git a/toolkit/components/passwordmgr/LoginManagerParent.jsm b/toolkit/components/passwordmgr/LoginManagerParent.jsm index 6974cc8ea4f9..a660e421e95d 100644 --- a/toolkit/components/passwordmgr/LoginManagerParent.jsm +++ b/toolkit/components/passwordmgr/LoginManagerParent.jsm @@ -22,6 +22,13 @@ XPCOMUtils.defineLazyGetter(this, "autocompleteFeature", () => { return new ExperimentFeature("password-autocomplete"); }); +XPCOMUtils.defineLazyGetter(this, "LoginRelatedRealmsParent", () => { + const { LoginRelatedRealmsParent } = ChromeUtils.import( + "resource://gre/modules/LoginRelatedRealms.jsm" + ); + return new LoginRelatedRealmsParent(); +}); + XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]); XPCOMUtils.defineLazyModuleGetters(this, { @@ -186,6 +193,7 @@ class LoginManagerParent extends JSWindowActorParent { * @param {origin?} options.httpRealm To match on. Omit this argument to match all realms. * @param {boolean} options.acceptDifferentSubdomains Include results for eTLD+1 matches * @param {boolean} options.ignoreActionAndRealm Include all form and HTTP auth logins for the site + * @param {string[]} options.relatedRealms Related realms to match against when searching */ static async searchAndDedupeLogins( formOrigin, @@ -194,6 +202,7 @@ class LoginManagerParent extends JSWindowActorParent { formActionOrigin, httpRealm, ignoreActionAndRealm, + relatedRealms, } = {} ) { let logins; @@ -209,6 +218,10 @@ class LoginManagerParent extends JSWindowActorParent { matchData.httpRealm = httpRealm; } } + if (LoginHelper.relatedRealmsEnabled) { + matchData.acceptRelatedRealms = LoginHelper.relatedRealmsEnabled; + matchData.relatedRealms = relatedRealms; + } try { logins = await Services.logins.searchLoginsAsync(matchData); } catch (e) { @@ -535,11 +548,22 @@ class LoginManagerParent extends JSWindowActorParent { origin: formOrigin, }); } else { + let relatedRealmsOrigins = []; + if (LoginHelper.relatedRealmsEnabled) { + relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms( + formOrigin + ); + } logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, { formActionOrigin: actionOrigin, ignoreActionAndRealm: true, acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup, + relatedRealms: relatedRealmsOrigins, }); + debug( + "Adding related logins on page load", + logins.map(l => l.origin) + ); } log("sendLoginDataToChild:", logins.length, "deduped logins"); @@ -606,12 +630,18 @@ class LoginManagerParent extends JSWindowActorParent { logins = LoginHelper.vanillaObjectsToLogins(previousResult.logins); } else { log("Creating new autocomplete search result."); - + let relatedRealmsOrigins = []; + if (LoginHelper.relatedRealmsEnabled) { + relatedRealmsOrigins = await LoginRelatedRealmsParent.findRelatedRealms( + formOrigin + ); + } // Autocomplete results do not need to match actionOrigin or exact origin. logins = await LoginManagerParent.searchAndDedupeLogins(formOrigin, { formActionOrigin: actionOrigin, ignoreActionAndRealm: true, acceptDifferentSubdomains: LoginHelper.includeOtherSubdomainsInLookup, + relatedRealms: relatedRealmsOrigins, }); } diff --git a/toolkit/components/passwordmgr/LoginRelatedRealms.jsm b/toolkit/components/passwordmgr/LoginRelatedRealms.jsm new file mode 100644 index 000000000000..76333e4763fe --- /dev/null +++ b/toolkit/components/passwordmgr/LoginRelatedRealms.jsm @@ -0,0 +1,118 @@ +/* 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/. */ + +"use strict"; + +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +XPCOMUtils.defineLazyModuleGetters(this, { + LoginHelper: "resource://gre/modules/LoginHelper.jsm", + RemoteSettings: "resource://services-settings/remote-settings.js", +}); + +XPCOMUtils.defineLazyGetter(this, "log", () => { + let logger = LoginHelper.createLogger("LoginRelatedRealms"); + return logger; +}); + +const EXPORTED_SYMBOLS = ["LoginRelatedRealmsParent"]; + +class LoginRelatedRealmsParent extends JSWindowActorParent { + /** + * @type RemoteSettingsClient + * + * @memberof LoginRelatedRealmsParent + */ + _sharedCredentialsClient = null; + /** + * @type string[][] + * + * @memberof LoginRelatedRealmsParent + */ + _relatedDomainsList = [[]]; + + /** + * Handles the Remote Settings sync event + * + * @param {Object} aEvent + * @param {Array} aEvent.current Records that are currently in the collection after the sync event + * @param {Array} aEvent.created Records that were created + * @param {Array} aEvent.updated Records that were updated + * @param {Array} aEvent.deleted Records that were deleted + * @memberof LoginRelatedRealmsParent + */ + onRemoteSettingsSync(aEvent) { + let { + data: { current }, + } = aEvent; + this._relatedDomainsList = current; + } + + async getSharedCredentialsCollection() { + if (!this._sharedCredentialsClient) { + this._sharedCredentialsClient = RemoteSettings( + LoginHelper.relatedRealmsCollection + ); + this._sharedCredentialsClient.on("sync", event => + this.onRemoteSettingsSync(event) + ); + this._relatedDomainsList = await this._sharedCredentialsClient.get(); + log.debug("Initialized related realms", this._relatedDomainsList); + } + log.debug("this._relatedDomainsList", this._relatedDomainsList); + return this._relatedDomainsList; + } + + /** + * Determine if there are any related realms of this `formOrigin` using the related realms collection + * @param {string} formOrigin A form origin + * @return {string[]} filteredRealms An array of domains related to the `formOrigin` + * @async + * @memberof LoginRelatedRealmsParent + */ + async findRelatedRealms(formOrigin) { + try { + let formOriginURI = Services.io.newURI(formOrigin); + let originDomain = formOriginURI.host; + let [ + { relatedRealms } = {}, + ] = await this.getSharedCredentialsCollection(); + if (!relatedRealms) { + return []; + } + let filterOriginIndex; + let shouldInclude = false; + let filteredRealms = relatedRealms.filter(_realms => { + for (let relatedOrigin of _realms) { + // We can't have an origin that matches multiple entries in our related realms collection + // so we exit the loop early + if (shouldInclude) { + return false; + } + if (Services.eTLD.hasRootDomain(originDomain, relatedOrigin)) { + shouldInclude = true; + break; + } + } + return shouldInclude; + }); + // * Filtered realms is a nested array due to its structure in Remote Settings + filteredRealms = filteredRealms.flat(); + + filterOriginIndex = filteredRealms.indexOf(originDomain); + // Removing the current formOrigin match if it exists in the related realms + // so that we don't return duplicates when we search for logins + if (filterOriginIndex !== -1) { + filteredRealms.splice(filterOriginIndex, 1); + } + return filteredRealms; + } catch (e) { + log.error(e); + return []; + } + } +} diff --git a/toolkit/components/passwordmgr/moz.build b/toolkit/components/passwordmgr/moz.build index 1ea21f43cbec..3409a4020ea2 100644 --- a/toolkit/components/passwordmgr/moz.build +++ b/toolkit/components/passwordmgr/moz.build @@ -41,6 +41,7 @@ EXTRA_JS_MODULES += [ "LoginManagerParent.jsm", "LoginManagerPrompter.jsm", "LoginRecipes.jsm", + "LoginRelatedRealms.jsm", "NewPasswordModel.jsm", "OSCrypto.jsm", "PasswordGenerator.jsm", diff --git a/toolkit/components/passwordmgr/storage-json.js b/toolkit/components/passwordmgr/storage-json.js index b547f82b14bc..f2bf720e0707 100644 --- a/toolkit/components/passwordmgr/storage-json.js +++ b/toolkit/components/passwordmgr/storage-json.js @@ -475,7 +475,9 @@ class LoginManagerStorage_json { // Some property names aren't field names but are special options to // affect the search. case "acceptDifferentSubdomains": - case "schemeUpgrades": { + case "schemeUpgrades": + case "acceptRelatedRealms": + case "relatedRealms": { options[prop.name] = prop.value; break; } @@ -508,6 +510,8 @@ class LoginManagerStorage_json { aOptions = { schemeUpgrades: false, acceptDifferentSubdomains: false, + acceptRelatedRealms: false, + relatedRealms: [], }, candidateLogins = this._store.data.logins ) { diff --git a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm index 93329dff431b..bbb6d0da9ebb 100644 --- a/toolkit/components/passwordmgr/test/LoginTestUtils.jsm +++ b/toolkit/components/passwordmgr/test/LoginTestUtils.jsm @@ -9,6 +9,14 @@ const EXPORTED_SYMBOLS = ["LoginTestUtils"]; +const { XPCOMUtils } = ChromeUtils.import( + "resource://gre/modules/XPCOMUtils.jsm" +); + +XPCOMUtils.defineLazyModuleGetters(this, { + RemoteSettings: "resource://services-settings/remote-settings.js", +}); + const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); let { Assert: AssertCls } = ChromeUtils.import( @@ -616,3 +624,27 @@ LoginTestUtils.file = { return tmpFile; }, }; + +LoginTestUtils.remoteSettings = { + relatedRealmsCollection: "websites-with-shared-credential-backends", + async setupWebsitesWithSharedCredentials( + relatedRealms = [["other-example.com", "example.com", "example.co.uk"]] + ) { + let db = await RemoteSettings(this.relatedRealmsCollection).db; + await db.clear(); + await db.create({ + id: "some-fake-ID-abc", + relatedRealms, + }); + await db.importChanges({}, 1234567); + }, + async cleanWebsitesWithSharedCredentials() { + let db = await RemoteSettings(this.relatedRealmsCollection).db; + await db.clear(); + await db.importChanges({}, 1234); + }, + async updateTimestamp() { + let db = await RemoteSettings(this.relatedRealmsCollection).db; + await db.importChanges({}, 12345678); + }, +}; diff --git a/toolkit/components/passwordmgr/test/browser/head.js b/toolkit/components/passwordmgr/test/browser/head.js index 0a2316c173e0..b3c504a6aeb6 100644 --- a/toolkit/components/passwordmgr/test/browser/head.js +++ b/toolkit/components/passwordmgr/test/browser/head.js @@ -28,6 +28,12 @@ add_task(async function common_initialize() { ["toolkit.telemetry.ipcBatchTimeout", 0], ], }); + if (LoginHelper.relatedRealmsEnabled) { + LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(); + registerCleanupFunction(function() { + LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials(); + }); + } }); registerCleanupFunction( diff --git a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini index 330bd1bc6e2d..079246d77c57 100644 --- a/toolkit/components/passwordmgr/test/mochitest/mochitest.ini +++ b/toolkit/components/passwordmgr/test/mochitest/mochitest.ini @@ -7,6 +7,8 @@ prefs = signon.testOnlyUserHasInteractedWithDocument=true security.insecure_field_warning.contextual.enabled=false network.auth.non-web-content-triggered-resources-http-auth-allow=true + # signon.relatedRealms.enabled pref needed until Bug 1699698 lands + signon.relatedRealms.enabled=true support-files = ../../../prompts/test/chromeScript.js @@ -38,6 +40,8 @@ skip-if = toolkit == 'android' && !is_fennec # Don't run on GeckoView # Note: new tests should use scheme = https unless they have a specific reason not to +[test_autocomplete_autofill_related_realms_no_dupes.html] +scheme = https [test_autocomplete_basic_form.html] skip-if = toolkit == 'android' || debug && (os == 'linux' || os == 'win') || os == 'linux' && tsan # android:autocomplete. Bug 1541945, Bug 1590928 scheme = https @@ -46,6 +50,8 @@ skip-if = toolkit == 'android' || os == 'linux' # android:autocomplete., linux: [test_autocomplete_basic_form_formActionOrigin.html] skip-if = toolkit == 'android' # android:autocomplete. scheme = https +[test_autocomplete_basic_form_related_realms.html] +scheme = https [test_autocomplete_basic_form_subdomain.html] skip-if = toolkit == 'android' # android:autocomplete. scheme = https diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js index 22a7b65583ab..75a47ef72fec 100644 --- a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js +++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common.js @@ -667,6 +667,23 @@ function resetRecipes() { }); } +function resetWebsitesWithSharedCredential() { + info("Resetting the 'websites-with-shared-credential-backend' collection"); + return new Promise(resolve => { + PWMGR_COMMON_PARENT.addMessageListener( + "resetWebsitesWithSharedCredential", + function reset() { + PWMGR_COMMON_PARENT.removeMessageListener( + "resetWebsitesWithSharedCredential", + reset + ); + resolve(); + } + ); + PWMGR_COMMON_PARENT.sendAsyncMessage("resetWebsitesWithSharedCredential"); + }); +} + function promiseStorageChanged(expectedChangeTypes) { return new Promise((resolve, reject) => { function onStorageChanged({ topic, data }) { diff --git a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js index a983950d03e6..93666911034c 100644 --- a/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js +++ b/toolkit/components/passwordmgr/test/mochitest/pwmgr_common_parent.js @@ -21,6 +21,12 @@ var { LoginManagerParent } = ChromeUtils.import( const { LoginTestUtils } = ChromeUtils.import( "resource://testing-common/LoginTestUtils.jsm" ); +if (LoginHelper.relatedRealmsEnabled) { + let rsPromise = LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(); + async () => { + await rsPromise; + }; +} var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); /** @@ -127,6 +133,7 @@ addMessageListener("cleanup", () => { Services.obs.removeObserver(onStorageChanged, "passwordmgr-storage-changed"); Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-change"); Services.obs.removeObserver(onPrompt, "passwordmgr-prompt-save"); + Services.logins.removeAllUserFacingLogins(); }); // Begin message listeners diff --git a/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html index a49aaaf78861..31d9683a483a 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_DOMInputPasswordAdded_fired_between_DOMContentLoaded_and_load_events.html @@ -25,7 +25,7 @@ let FILE_PATH = "/tests/toolkit/components/passwordmgr/test/mochitest/slow_image runInParent(function removeAll() { let {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); Services.logins.removeAllUserFacingLogins(); -}) +}); let readyPromise = registerRunTests(); diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html new file mode 100644 index 000000000000..b32ae69e34b1 --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_autofill_related_realms_no_dupes.html @@ -0,0 +1,101 @@ + + + + + Test no duplicate logins using autofill/autocomplete with related realms + + + + + + + +Login Manager test: no duplicate logins when using autofill and autocomplete with related realms + +

+
+
+ + + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html new file mode 100644 index 000000000000..3951e9e19eed --- /dev/null +++ b/toolkit/components/passwordmgr/test/mochitest/test_autocomplete_basic_form_related_realms.html @@ -0,0 +1,131 @@ + + + + + Test login autocomplete with related realms + + + + + + + +Login Manager test: related realms autocomplete + + +

+
+
+ + + +
+ +
+ +
+
+
+ + diff --git a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html index 4bbfa2f1d4b2..0008087dc074 100644 --- a/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html +++ b/toolkit/components/passwordmgr/test/mochitest/test_username_focus.html @@ -240,10 +240,6 @@ add_task(async function test_multiple_prefilled_focused_dynamic() { usernameField.focus(); await shownPromise; }); - -add_task(async function cleanup() { - removeFocus(); -}); diff --git a/toolkit/components/passwordmgr/test/unit/head.js b/toolkit/components/passwordmgr/test/unit/head.js index e7c9205257d3..57682221b114 100644 --- a/toolkit/components/passwordmgr/test/unit/head.js +++ b/toolkit/components/passwordmgr/test/unit/head.js @@ -52,7 +52,7 @@ const newPropertyBag = LoginHelper.newPropertyBag; const NEW_PASSWORD_HEURISTIC_ENABLED_PREF = "signon.generation.confidenceThreshold"; - +const RELATED_REALMS_ENABLED_PREF = "signon.relatedRealms.enabled"; /** * All the tests are implemented with add_task, this starts them automatically. */ @@ -91,6 +91,12 @@ add_task(async function test_common_initialize() { // Ensure that the service and the storage module are initialized. await Services.logins.initializationPromise; + Services.prefs.setBoolPref(RELATED_REALMS_ENABLED_PREF, true); + if (LoginHelper.relatedRealmsEnabled) { + // Ensure that there is a mocked Remote Settings database for the + // "websites-with-shared-credential-backends" collection + await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials(); + } }); add_task(async function test_common_prefs() { diff --git a/toolkit/components/passwordmgr/test/unit/test_findRelatedRealms.js b/toolkit/components/passwordmgr/test/unit/test_findRelatedRealms.js new file mode 100644 index 000000000000..1f9bbc718c71 --- /dev/null +++ b/toolkit/components/passwordmgr/test/unit/test_findRelatedRealms.js @@ -0,0 +1,157 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { LoginRelatedRealmsParent } = ChromeUtils.import( + "resource://gre/modules/LoginRelatedRealms.jsm" +); +const { RemoteSettings } = ChromeUtils.import( + "resource://services-settings/remote-settings.js", + {} +); + +const REMOTE_SETTINGS_COLLECTION = "websites-with-shared-credential-backends"; + +add_task(async function test_related_domain_matching() { + const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION); + const records = await client.get(); + console.log(records); + + // Assumes that the test collection is a 2D array with one subarray + let relatedRealms = records[0].relatedRealms; + relatedRealms = relatedRealms.flat(); + ok(relatedRealms); + + let LRR = new LoginRelatedRealmsParent(); + + // We should not return unrelated realms + let result = await LRR.findRelatedRealms("https://not-example.com"); + equal(result.length, 0, "Check that there were no related realms found"); + + // We should not return unrelated realms given an unrelated subdomain + result = await LRR.findRelatedRealms("https://sub.not-example.com"); + equal(result.length, 0, "Check that there were no related realms found"); + // We should return the related realms collection + result = await LRR.findRelatedRealms("https://sub.example.com"); + equal( + result.length, + relatedRealms.length, + "Ensure that three related realms were found" + ); + + // We should return the related realms collection minus the base domain that we searched with + result = await LRR.findRelatedRealms("https://example.co.uk"); + equal( + result.length, + relatedRealms.length - 1, + "Ensure that two related realms were found" + ); +}); + +add_task(async function test_newly_synced_collection() { + // Initialize LoginRelatedRealmsParent so the sync handler is enabled + let LRR = new LoginRelatedRealmsParent(); + await LRR.getSharedCredentialsCollection(); + + const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION); + let records = await client.get(); + const record1 = { + id: records[0].id, + relatedRealms: records[0].relatedRealms, + }; + + // Assumes that the test collection is a 2D array with one subarray + let originalRelatedRealms = records[0].relatedRealms; + originalRelatedRealms = originalRelatedRealms.flat(); + ok(originalRelatedRealms); + + const updatedRelatedRealms = ["completely-different.com", "example.com"]; + const record2 = { + id: "some-other-ID", + relatedRealms: [updatedRelatedRealms], + }; + const payload = { + current: [record2], + created: [record2], + updated: [], + deleted: [record1], + }; + await RemoteSettings(REMOTE_SETTINGS_COLLECTION).emit("sync", { + data: payload, + }); + + let [{ id, relatedRealms }] = await LRR.getSharedCredentialsCollection(); + equal(id, record2.id, "internal collection ID should be updated"); + equal( + relatedRealms, + record2.relatedRealms, + "internal collection related realms should be updated" + ); + + // We should return only one result, and that result should be example.com + // NOT other-example.com or example.co.uk + let result = await LRR.findRelatedRealms("https://completely-different.com"); + equal( + result.length, + updatedRelatedRealms.length - 1, + "Check that there is only one related realm found" + ); + equal( + result[0], + "example.com", + "Ensure that the updated collection should only match example.com" + ); +}); + +add_task(async function test_no_related_domains() { + await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials(); + + const client = RemoteSettings(REMOTE_SETTINGS_COLLECTION); + let records = await client.get(); + + equal(records.length, 0, "Check that there are no related realms"); + + let LRR = new LoginRelatedRealmsParent(); + + ok(LRR.findRelatedRealms, "Ensure findRelatedRealms exists"); + + let result = await LRR.findRelatedRealms("https://example.com"); + equal(result.length, 0, "Assert that there were no related realms found"); +}); + +add_task(async function test_unrelated_subdomains() { + await LoginTestUtils.remoteSettings.cleanWebsitesWithSharedCredentials(); + let testCollection = [ + ["slpl.bibliocommons.com", "slpl.overdrive.com"], + ["springfield.overdrive.com", "coolcat.org"], + ]; + await LoginTestUtils.remoteSettings.setupWebsitesWithSharedCredentials( + testCollection + ); + + let LRR = new LoginRelatedRealmsParent(); + let result = await LRR.findRelatedRealms("https://evil.overdrive.com"); + equal(result.length, 0, "Assert that there were no related realms found"); + + result = await LRR.findRelatedRealms("https://abc.slpl.bibliocommons.com"); + equal(result.length, 2, "Assert that two related realms were found"); + equal(result[0], testCollection[0][0]); + equal(result[1], testCollection[0][1]); + + result = await LRR.findRelatedRealms("https://slpl.overdrive.com"); + console.log("what is result: " + result); + equal(result.length, 1, "Assert that one related realm was found"); + for (let item of result) { + notEqual( + item, + "coolcat.org", + "coolcat.org is not related to slpl.overdrive.com" + ); + notEqual( + item, + "springfield.overdrive.com", + "springfield.overdrive.com is not related to slpl.overdrive.com" + ); + } +}); diff --git a/toolkit/components/passwordmgr/test/unit/test_remote_recipes.js b/toolkit/components/passwordmgr/test/unit/test_remote_recipes.js index 4197865f09b2..07ef3055f6bf 100644 --- a/toolkit/components/passwordmgr/test/unit/test_remote_recipes.js +++ b/toolkit/components/passwordmgr/test/unit/test_remote_recipes.js @@ -3,6 +3,7 @@ /** * Tests retrieving remote LoginRecipes in the parent process. + * See https://firefox-source-docs.mozilla.org/services/settings/#unit-tests for explanation of db.importChanges({}, 42); */ "use strict"; @@ -16,13 +17,14 @@ const REMOTE_SETTINGS_COLLECTION = "password-recipes"; add_task(async function test_init_remote_recipe() { const db = await RemoteSettings(REMOTE_SETTINGS_COLLECTION).db; + await db.clear(); const record1 = { id: "some-fake-ID", hosts: ["www.testDomain.com"], description: "Some description here", usernameSelector: "#username", }; - await db.create(record1); + await db.importChanges({}, 42, [record1], { clear: true }); let parent = new LoginRecipesParent({ defaults: true }); let recipesParent = await parent.initializationPromise; @@ -54,6 +56,7 @@ add_task(async function test_init_remote_recipe() { "Initially 1 recipe based on our test record" ); await db.clear(); + await db.importChanges({}, 42); }); add_task(async function test_add_recipe_sync() { @@ -64,7 +67,7 @@ add_task(async function test_add_recipe_sync() { description: "Some description here", usernameSelector: "#username", }; - await db.create(record1); + await db.importChanges({}, 42, [record1], { clear: true }); let parent = new LoginRecipesParent({ defaults: true }); let recipesParent = await parent.initializationPromise; @@ -89,6 +92,7 @@ add_task(async function test_add_recipe_sync() { "New recipe from sync event added successfully" ); await db.clear(); + await db.importChanges({}, 42); }); add_task(async function test_remove_recipe_sync() { @@ -99,7 +103,7 @@ add_task(async function test_remove_recipe_sync() { description: "Some description here", usernameSelector: "#username", }; - await db.create(record1); + await db.importChanges({}, 42, [record1], { clear: true }); let parent = new LoginRecipesParent({ defaults: true }); let recipesParent = await parent.initializationPromise; @@ -129,7 +133,7 @@ add_task(async function test_malformed_recipes_in_db() { usernameSelector: "#username", fieldThatDoesNotExist: "value", }; - await db.create(malformedRecord); + await db.importChanges({}, 42, [malformedRecord], { clear: true }); let parent = new LoginRecipesParent({ defaults: true }); try { await parent.initializationPromise; @@ -146,7 +150,7 @@ add_task(async function test_malformed_recipes_in_db() { description: "Some description here", usernameSelector: "#username", }; - await db.create(missingHostsRecord); + await db.importChanges({}, 42, [missingHostsRecord], { clear: true }); parent = new LoginRecipesParent({ defaults: true }); try { await parent.initializationPromise; diff --git a/toolkit/components/passwordmgr/test/unit/xpcshell.ini b/toolkit/components/passwordmgr/test/unit/xpcshell.ini index a1c343fa379f..3fbcfdca6357 100644 --- a/toolkit/components/passwordmgr/test/unit/xpcshell.ini +++ b/toolkit/components/passwordmgr/test/unit/xpcshell.ini @@ -18,6 +18,7 @@ run-if = buildapp == "browser" [test_disabled_hosts.js] [test_displayOrigin.js] [test_doLoginsMatch.js] +[test_findRelatedRealms.js] [test_getFormFields.js] [test_getPasswordFields.js] [test_getPasswordOrigin.js]