mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-13115] Allow users to disable extension content script injections by domain (#11826)
* add disabledInteractionsUris state to the domain settings service * add routes and ui for user disabledInteractionsUris state management * use disabled URIs service state as a preemptive conditon to injecting content scripts * move disabled domains navigation button from account security settings to autofill settings * update disabled domain terminology to blocked domain terminology * update copy * handle blocked domains initializing with null value * add dismissable banner to the vault view when the active autofill tab is on the blocked domains list * add autofill blocked domain indicators to autofill suggestions section header * add BlockBrowserInjectionsByDomain feature flag and put feature behind it * update router config to new style * update tests and cleanup * use full-width-notice slot for domain script injection blocked banner * convert thrown error on content script injection block to a warning and early return * simplify and enspeeden state resolution for blockedInteractionsUris * refactor feature flag state fetching and update tests * document domain settings service * remove vault component presentational updates
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { MockProxy, mock } from "jest-mock-extended";
|
||||
import { firstValueFrom, of } from "rxjs";
|
||||
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
|
||||
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { UserId } from "../../types/guid";
|
||||
@@ -8,6 +11,7 @@ import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-se
|
||||
|
||||
describe("DefaultDomainSettingsService", () => {
|
||||
let domainSettingsService: DomainSettingsService;
|
||||
let configService: MockProxy<ConfigService>;
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||
@@ -19,10 +23,13 @@ describe("DefaultDomainSettingsService", () => {
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
|
||||
configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockImplementation(() => of(false));
|
||||
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider, configService);
|
||||
|
||||
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
|
||||
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
|
||||
domainSettingsService.blockedInteractionsUris$ = of(null);
|
||||
});
|
||||
|
||||
describe("getUrlEquivalentDomains", () => {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { map, Observable } from "rxjs";
|
||||
import { map, Observable, switchMap, of } from "rxjs";
|
||||
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
|
||||
import {
|
||||
NeverDomains,
|
||||
@@ -8,6 +10,7 @@ import {
|
||||
UriMatchStrategySetting,
|
||||
UriMatchStrategy,
|
||||
} from "../../models/domain/domain-service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
DOMAIN_SETTINGS_DISK,
|
||||
@@ -23,10 +26,20 @@ const SHOW_FAVICONS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "showFavicons", {
|
||||
deserializer: (value: boolean) => value ?? true,
|
||||
});
|
||||
|
||||
// Domain exclusion list for notifications
|
||||
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
|
||||
deserializer: (value: NeverDomains) => value ?? null,
|
||||
});
|
||||
|
||||
// Domain exclusion list for content script injections
|
||||
const BLOCKED_INTERACTIONS_URIS = new KeyDefinition(
|
||||
DOMAIN_SETTINGS_DISK,
|
||||
"blockedInteractionsUris",
|
||||
{
|
||||
deserializer: (value: NeverDomains) => value ?? null,
|
||||
},
|
||||
);
|
||||
|
||||
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
|
||||
deserializer: (value: EquivalentDomains) => value ?? null,
|
||||
clearOn: ["logout"],
|
||||
@@ -41,15 +54,45 @@ const DEFAULT_URI_MATCH_STRATEGY = new UserKeyDefinition(
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* The Domain Settings service; provides client settings state for "active client view" URI concerns
|
||||
*/
|
||||
export abstract class DomainSettingsService {
|
||||
/**
|
||||
* Indicates if the favicons for ciphers' URIs should be shown instead of a placeholder
|
||||
*/
|
||||
showFavicons$: Observable<boolean>;
|
||||
setShowFavicons: (newValue: boolean) => Promise<void>;
|
||||
|
||||
/**
|
||||
* User-specified URIs for which the client notifications should not appear
|
||||
*/
|
||||
neverDomains$: Observable<NeverDomains>;
|
||||
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
|
||||
|
||||
/**
|
||||
* User-specified URIs for which client content script injections should not occur, and the state
|
||||
* of banner/notice visibility for those domains within the client
|
||||
*/
|
||||
blockedInteractionsUris$: Observable<NeverDomains>;
|
||||
setBlockedInteractionsUris: (newValue: NeverDomains) => Promise<void>;
|
||||
|
||||
/**
|
||||
* URIs which should be treated as equivalent to each other for various concerns (autofill, etc)
|
||||
*/
|
||||
equivalentDomains$: Observable<EquivalentDomains>;
|
||||
setEquivalentDomains: (newValue: EquivalentDomains, userId: UserId) => Promise<void>;
|
||||
|
||||
/**
|
||||
* User-specified default for URI-matching strategies (for example, when determining relevant
|
||||
* ciphers for an active browser tab). Can be overridden by cipher-specific settings.
|
||||
*/
|
||||
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
|
||||
|
||||
/**
|
||||
* Helper function for the common resolution of a given URL against equivalent domains
|
||||
*/
|
||||
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
|
||||
}
|
||||
|
||||
@@ -60,19 +103,37 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
|
||||
private neverDomainsState: GlobalState<NeverDomains>;
|
||||
readonly neverDomains$: Observable<NeverDomains>;
|
||||
|
||||
private blockedInteractionsUrisState: GlobalState<NeverDomains>;
|
||||
readonly blockedInteractionsUris$: Observable<NeverDomains>;
|
||||
|
||||
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
|
||||
readonly equivalentDomains$: Observable<EquivalentDomains>;
|
||||
|
||||
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
|
||||
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
constructor(
|
||||
private stateProvider: StateProvider,
|
||||
private configService: ConfigService,
|
||||
) {
|
||||
this.showFaviconsState = this.stateProvider.getGlobal(SHOW_FAVICONS);
|
||||
this.showFavicons$ = this.showFaviconsState.state$.pipe(map((x) => x ?? true));
|
||||
|
||||
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
|
||||
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
|
||||
|
||||
// Needs to be global to prevent pre-login injections
|
||||
this.blockedInteractionsUrisState = this.stateProvider.getGlobal(BLOCKED_INTERACTIONS_URIS);
|
||||
|
||||
this.blockedInteractionsUris$ = this.configService
|
||||
.getFeatureFlag$(FeatureFlag.BlockBrowserInjectionsByDomain)
|
||||
.pipe(
|
||||
switchMap((featureIsEnabled) =>
|
||||
featureIsEnabled ? this.blockedInteractionsUrisState.state$ : of({} as NeverDomains),
|
||||
),
|
||||
map((disabledUris) => (Object.keys(disabledUris).length ? disabledUris : null)),
|
||||
);
|
||||
|
||||
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
|
||||
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
|
||||
|
||||
@@ -90,6 +151,10 @@ export class DefaultDomainSettingsService implements DomainSettingsService {
|
||||
await this.neverDomainsState.update(() => newValue);
|
||||
}
|
||||
|
||||
async setBlockedInteractionsUris(newValue: NeverDomains): Promise<void> {
|
||||
await this.blockedInteractionsUrisState.update(() => newValue);
|
||||
}
|
||||
|
||||
async setEquivalentDomains(newValue: EquivalentDomains, userId: UserId): Promise<void> {
|
||||
await this.stateProvider.getUser(userId, EQUIVALENT_DOMAINS).update(() => newValue);
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ export enum FeatureFlag {
|
||||
SSHKeyVaultItem = "ssh-key-vault-item",
|
||||
SSHAgent = "ssh-agent",
|
||||
NotificationBarAddLoginImprovements = "notification-bar-add-login-improvements",
|
||||
BlockBrowserInjectionsByDomain = "block-browser-injections-by-domain",
|
||||
AC2476_DeprecateStripeSourcesAPI = "AC-2476-deprecate-stripe-sources-api",
|
||||
CipherKeyEncryption = "cipher-key-encryption",
|
||||
VerifiedSsoDomainEndpoint = "pm-12337-refactor-sso-details-endpoint",
|
||||
@@ -81,6 +82,7 @@ export const DefaultFeatureFlagValue = {
|
||||
[FeatureFlag.SSHKeyVaultItem]: FALSE,
|
||||
[FeatureFlag.SSHAgent]: FALSE,
|
||||
[FeatureFlag.NotificationBarAddLoginImprovements]: FALSE,
|
||||
[FeatureFlag.BlockBrowserInjectionsByDomain]: FALSE,
|
||||
[FeatureFlag.AC2476_DeprecateStripeSourcesAPI]: FALSE,
|
||||
[FeatureFlag.CipherKeyEncryption]: FALSE,
|
||||
[FeatureFlag.VerifiedSsoDomainEndpoint]: FALSE,
|
||||
|
||||
@@ -21,5 +21,5 @@ export const UriMatchStrategy = {
|
||||
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||
|
||||
// using uniqueness properties of object shape over Set for ease of state storability
|
||||
export type NeverDomains = { [id: string]: null };
|
||||
export type NeverDomains = { [id: string]: null | { bannerIsDismissed?: boolean } };
|
||||
export type EquivalentDomains = string[][];
|
||||
|
||||
Reference in New Issue
Block a user