commit 11e03be7e93435c02bcb061003ec663ce70f1c55
parent 21f0d24306349a194bb883c002733841f9d86bd7
Author: groovecoder <71928+groovecoder@users.noreply.github.com>
Date: Wed, 1 Oct 2025 15:24:56 +0000
Bug 1991452 - feat(relay): support denylist filtering of Firefox Relay autocomplete r=credential-management-reviewers,vprema,dimi
Don't show Firefox Relay autocomplete on sites on a new Remote Settings-based denylist,
complementing the existing allowlist logic. This allows finer-grained control over which
sites are offered Relay addresses, especially when the user is signed in.
- Introduced `fxrelay-denylist` Remote Settings collection.
- Updated `FirefoxRelay.sys.mjs` to check denylist entries via a new `onDenyList` function.
- Refactored list loading logic into a reusable `getListCollection()` helper.
- Adjusted test utilities to support stubbing both allow and deny lists.
- Added a new browser test (`browser_relay_use.js`) to verify denylist behavior when a user is signed in.
Differential Revision: https://phabricator.services.mozilla.com/D266838
Diffstat:
6 files changed, 140 insertions(+), 35 deletions(-)
diff --git a/toolkit/components/passwordmgr/test/browser/browser.toml b/toolkit/components/passwordmgr/test/browser/browser.toml
@@ -257,6 +257,9 @@ support-files = ["browser_relay_utils.js"]
["browser_relay_telemetry.js"]
support-files = ["browser_relay_utils.js"]
+["browser_relay_use.js"]
+support-files = ["browser_relay_utils.js"]
+
["browser_telemetry_SignUpFormRuleset.js"]
["browser_test_changeContentInputValue.js"]
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow_showToAllBrowsers.js b/toolkit/components/passwordmgr/test/browser/browser_relay_signup_flow_showToAllBrowsers.js
@@ -111,7 +111,7 @@ add_task(
Assert.equal(
rsSandbox.getFakes()[0].callCount,
rsSandboxRemoteSettingsGetCallsBeforeSecondACPopup,
- "FirefoxRelay onAllowList should only call RemoteSettings.get() once."
+ "FirefoxRelay shouldShowRelay should only call RemoteSettings.get() once."
);
rsSandbox.restore();
}
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js b/toolkit/components/passwordmgr/test/browser/browser_relay_telemetry.js
@@ -5,13 +5,6 @@ Services.scriptloader.loadSubScript(
const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
-const setupRelayScenario = async scenarioName => {
- await SpecialPowers.pushPrefEnv({
- set: [["signon.firefoxRelay.feature", scenarioName]],
- });
- Services.telemetry.clearEvents();
-};
-
const collectRelayTelemeryEvent = sameFlow => {
const collectedEvents = TelemetryTestUtils.getEvents(
{ category: "relay_integration" },
@@ -263,7 +256,8 @@ add_task(async function test_popup_option_optin_disabled() {
add_task(async function test_popup_option_fillusername() {
await setupRelayScenario("enabled");
- const rsSandbox = await stubRemoteSettingsAllowList();
+ const rsAllowSandbox = await stubRemoteSettingsAllowList();
+ const rsDenySandbox = await stubRemoteSettingsDenyList();
await BrowserTestUtils.withNewTab(
{
gBrowser,
@@ -284,7 +278,8 @@ add_task(async function test_popup_option_fillusername() {
]);
}
);
- rsSandbox.restore();
+ rsAllowSandbox.restore();
+ rsDenySandbox.restore();
});
add_task(async function test_fillusername_free_tier_limit() {
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_use.js b/toolkit/components/passwordmgr/test/browser/browser_relay_use.js
@@ -0,0 +1,35 @@
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js",
+ this
+);
+
+const TEST_URL_PATH = `https://example.org${DIRECTORY_PATH}form_basic_signup.html`;
+
+add_task(
+ async function test_site_on_denyList_does_not_show_Relay_to_signed_in_browser() {
+ await setupRelayScenario("enabled");
+ const sandbox = stubFxAccountsToSimulateSignedIn();
+ // Set up denylist for "example.org"
+ const rsSandbox = await stubRemoteSettingsDenyList([
+ { domain: "example.org" },
+ ]);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL_PATH,
+ },
+ async function (browser) {
+ const popup = document.getElementById("PopupAutoComplete");
+ await openACPopup(popup, browser, "#form-basic-username");
+
+ const relayItem = getRelayItemFromACPopup(popup);
+ Assert.ok(
+ !relayItem,
+ "Relay item SHOULD NOT be present in the autocomplete popup when the site is on the deny-list, even if the user is signed into the browser."
+ );
+ }
+ );
+ sandbox.restore();
+ rsSandbox.restore();
+ }
+);
diff --git a/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js b/toolkit/components/passwordmgr/test/browser/browser_relay_utils.js
@@ -107,12 +107,30 @@ async function stubRemoteSettingsAllowList(
allowList = [{ domain: "example.org" }]
) {
const allowListRS = await lazy.RemoteSettings("fxrelay-allowlist");
+ // If already stubbed, restore
+ if (allowListRS.get && allowListRS.get.restore) {
+ allowListRS.get.restore();
+ }
const rsSandbox = sinon.createSandbox();
rsSandbox.stub(allowListRS, "get").returns(allowList);
allowListRS.emit("sync");
return rsSandbox;
}
+async function stubRemoteSettingsDenyList(
+ denyList = [{ domain: "on-denylist.org" }]
+) {
+ const denyListRS = await lazy.RemoteSettings("fxrelay-denylist");
+ // If already stubbed, restore
+ if (denyListRS.get && denyListRS.get.restore) {
+ denyListRS.get.restore();
+ }
+ const rsSandbox = sinon.createSandbox();
+ rsSandbox.stub(denyListRS, "get").returns(denyList);
+ denyListRS.emit("sync");
+ return rsSandbox;
+}
+
add_setup(async function () {
const allMessageIds = [];
for (const key in autocompleteUXTreatments) {
@@ -179,3 +197,10 @@ async function clickButtonAndWaitForPopupToClose(buttonToClick) {
buttonToClick.click();
await notificationHiddenEvent;
}
+
+const setupRelayScenario = async scenarioName => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["signon.firefoxRelay.feature", scenarioName]],
+ });
+ Services.telemetry.clearEvents();
+};
diff --git a/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs b/toolkit/components/satchel/integrations/FirefoxRelay.sys.mjs
@@ -38,8 +38,11 @@ const gConfig = (function () {
"signon.firefoxRelay.privacy_policy_url"
),
allowListForFirstOfferPref: "signon.firefoxRelay.allowListForFirstOffer",
+ denyListForFutureOffersPref: "signon.firefoxRelay.denyListForFutureOffers",
allowListRemoteSettingsCollectionPref:
"signon.firefoxRelay.allowListRemoteSettingsCollection",
+ denyListRemoteSettingsCollectionPref:
+ "signon.firefoxRelay.denyListRemoteSettingsCollection",
};
})();
@@ -108,6 +111,7 @@ const AUTH_TOKEN_ERROR_CODE = 418;
let gFlowId;
let gAllowListCollection;
+let gDenyListCollection;
async function getRelayTokenAsync() {
try {
@@ -276,6 +280,7 @@ function getDisableIntegration(disableStrings, feature) {
},
};
}
+
async function showReusableMasksAsync(browser, origin, error) {
const [reusableMasks, status] = await getReusableMasksAsync(browser, origin);
if (!reusableMasks) {
@@ -466,44 +471,85 @@ function isSignup(scenarioName) {
return scenarioName == "SignUpFormScenario";
}
-async function onAllowList(origin) {
- const allowListForFirstOffer = Services.prefs.getBoolPref(
- gConfig.allowListForFirstOfferPref,
- true
- );
- if (!allowListForFirstOffer) {
- return true;
- }
- if (!origin) {
- return false;
- }
- if (!gAllowListCollection) {
- const allowListRemoteSettingsCollection = Services.prefs.getStringPref(
- gConfig.allowListRemoteSettingsCollectionPref,
- "fxrelay-allowlist"
+// Helper to load/cache RemoteSettings collections
+async function getListCollection({
+ cache,
+ setCache,
+ collectionPref,
+ defaultCollection,
+}) {
+ if (!cache()) {
+ const collectionName = Services.prefs.getStringPref(
+ gConfig[collectionPref],
+ defaultCollection
);
try {
- gAllowListCollection = await lazy
- .RemoteSettings(allowListRemoteSettingsCollection)
- .get();
- lazy.RemoteSettings(allowListRemoteSettingsCollection).on("sync", () => {
- gAllowListCollection = null;
+ const list = await lazy.RemoteSettings(collectionName).get();
+ setCache(list);
+ lazy.RemoteSettings(collectionName).on("sync", () => {
+ setCache(null);
});
} catch (ex) {
if (ex instanceof lazy.RemoteSettingsClient.UnknownCollectionError) {
lazy.log.warn(
"Could not get Remote Settings collection.",
- gConfig.allowListRemoteSettingsCollection,
+ collectionPref,
ex
);
}
throw ex;
}
}
+ return cache();
+}
+
+function isOriginInList(list, origin) {
const originHost = new URL(origin).host;
- return gAllowListCollection.some(
- allowListRecord => allowListRecord.domain == originHost
+ return list.some(record => record.domain == originHost);
+}
+
+async function shouldNotShowRelay(origin) {
+ const denyListForFutureOffers = Services.prefs.getBoolPref(
+ gConfig.denyListForFutureOffersPref,
+ true
);
+ if (!denyListForFutureOffers) {
+ return false;
+ }
+ if (!origin) {
+ return true;
+ }
+ const list = await getListCollection({
+ cache: () => gDenyListCollection,
+ setCache: v => {
+ gDenyListCollection = v;
+ },
+ collectionPref: "denyListRemoteSettingsCollectionPref",
+ defaultCollection: "fxrelay-denylist",
+ });
+ return isOriginInList(list, origin);
+}
+
+async function shouldShowRelay(origin) {
+ const allowListForFirstOffer = Services.prefs.getBoolPref(
+ gConfig.allowListForFirstOfferPref,
+ true
+ );
+ if (!allowListForFirstOffer) {
+ return true;
+ }
+ if (!origin) {
+ return false;
+ }
+ const list = await getListCollection({
+ cache: () => gAllowListCollection,
+ setCache: v => {
+ gAllowListCollection = v;
+ },
+ collectionPref: "allowListRemoteSettingsCollectionPref",
+ defaultCollection: "fxrelay-allowlist",
+ });
+ return isOriginInList(list, origin);
}
class RelayOffered {
@@ -511,7 +557,7 @@ class RelayOffered {
const hasFxA = await hasFirefoxAccountAsync();
const showRelayOnAllowlistSiteToAllUsers =
Services.prefs.getBoolPref(gConfig.showToAllBrowsersPref, false) &&
- (await onAllowList(origin));
+ (await shouldShowRelay(origin));
if (
!hasInput &&
isSignup(scenarioName) &&
@@ -869,7 +915,8 @@ class RelayOffered {
class RelayEnabled {
async *autocompleteItemsAsync(origin, scenarioName, hasInput) {
- if (!hasInput && isSignup(scenarioName)) {
+ const originOnDenyList = await shouldNotShowRelay(origin);
+ if (!hasInput && isSignup(scenarioName) && !originOnDenyList) {
const hasFxA = await hasFirefoxAccountAsync();
const [title] = await formatMessages("firefox-relay-use-mask-title-1");