[PM-5562] Implement Domain Settings state provider (#8226)

* create domain settings state provider

* replace callsites for defaultUriMatch and neverDomains with DomainSettingsService equivalents

* replace callsites for equivalentDomains with DomainSettingsService equivalents and clean up unused AccountSettingsSettings

* add migrations for domain settings state

* do not use enum for URI match strategy constants and types

* add getUrlEquivalentDomains test

* PR suggestions/cleanup

* refactor getUrlEquivalentDomains to return an observable

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>

* update tests

* add UriMatchStrategy docs notes

* service class renames

* use service abstraction at callsites previously using service class directly

---------

Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com>
Co-authored-by:  Audrey  <ajensen@bitwarden.com>
This commit is contained in:
Jonathan Prusik 2024-03-12 15:07:14 -04:00 committed by GitHub
parent a0e0637bb6
commit 0a595ea95e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 945 additions and 455 deletions

View File

@ -1,3 +1,4 @@
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum";
@ -111,6 +112,7 @@ type NotificationBackgroundExtensionMessageHandlers = {
collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>; collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise<void>;
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>; bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
bgGetEnableAddedLoginPrompt: () => Promise<boolean>; bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
bgGetExcludedDomains: () => Promise<NeverDomains>;
getWebVaultUrlForNotification: () => string; getWebVaultUrlForNotification: () => string;
}; };

View File

@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs";
import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { AuthService } from "@bitwarden/common/auth/services/auth.service"; import { AuthService } from "@bitwarden/common/auth/services/auth.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsService } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service";
@ -47,6 +48,7 @@ describe("NotificationBackground", () => {
const folderService = mock<FolderService>(); const folderService = mock<FolderService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const userNotificationSettingsService = mock<UserNotificationSettingsService>(); const userNotificationSettingsService = mock<UserNotificationSettingsService>();
const domainSettingsService = mock<DomainSettingsService>();
const environmentService = mock<EnvironmentService>(); const environmentService = mock<EnvironmentService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
@ -59,6 +61,7 @@ describe("NotificationBackground", () => {
folderService, folderService,
stateService, stateService,
userNotificationSettingsService, userNotificationSettingsService,
domainSettingsService,
environmentService, environmentService,
logService, logService,
); );

View File

@ -5,7 +5,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants"; import { NOTIFICATION_BAR_LIFESPAN_MS } from "@bitwarden/common/autofill/constants";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { NeverDomains } from "@bitwarden/common/models/domain/domain-service";
import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
@ -60,6 +62,7 @@ export default class NotificationBackground {
bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab),
bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(),
bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(),
bgGetExcludedDomains: () => this.getExcludedDomains(),
getWebVaultUrlForNotification: () => this.getWebVaultUrl(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(),
}; };
@ -71,6 +74,7 @@ export default class NotificationBackground {
private folderService: FolderService, private folderService: FolderService,
private stateService: BrowserStateService, private stateService: BrowserStateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private environmentService: EnvironmentService, private environmentService: EnvironmentService,
private logService: LogService, private logService: LogService,
) {} ) {}
@ -99,6 +103,13 @@ export default class NotificationBackground {
return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$); return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$);
} }
/**
* Gets the neverDomains setting from the domain settings service.
*/
async getExcludedDomains(): Promise<NeverDomains> {
return await firstValueFrom(this.domainSettingsService.neverDomains$);
}
/** /**
* Checks the notification queue for any messages that need to be sent to the * Checks the notification queue for any messages that need to be sent to the
* specified tab. If no tab is specified, the current tab will be used. * specified tab. If no tab is specified, the current tab will be used.

View File

@ -6,10 +6,6 @@ import {
EventCollectionServiceInitOptions, EventCollectionServiceInitOptions,
eventCollectionServiceFactory, eventCollectionServiceFactory,
} from "../../../background/service-factories/event-collection-service.factory"; } from "../../../background/service-factories/event-collection-service.factory";
import {
settingsServiceFactory,
SettingsServiceInitOptions,
} from "../../../background/service-factories/settings-service.factory";
import { import {
CachedServices, CachedServices,
factory, factory,
@ -38,6 +34,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "./autofill-settings-service.factory"; } from "./autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "./domain-settings-service.factory";
type AutoFillServiceOptions = FactoryOptions; type AutoFillServiceOptions = FactoryOptions;
@ -48,8 +48,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions &
TotpServiceInitOptions & TotpServiceInitOptions &
EventCollectionServiceInitOptions & EventCollectionServiceInitOptions &
LogServiceInitOptions & LogServiceInitOptions &
SettingsServiceInitOptions & UserVerificationServiceInitOptions &
UserVerificationServiceInitOptions; DomainSettingsServiceInitOptions;
export function autofillServiceFactory( export function autofillServiceFactory(
cache: { autofillService?: AbstractAutoFillService } & CachedServices, cache: { autofillService?: AbstractAutoFillService } & CachedServices,
@ -67,7 +67,7 @@ export function autofillServiceFactory(
await totpServiceFactory(cache, opts), await totpServiceFactory(cache, opts),
await eventCollectionServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts),
await logServiceFactory(cache, opts), await logServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await userVerificationServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts),
), ),
); );

View File

@ -0,0 +1,25 @@
import { DefaultDomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import {
CachedServices,
factory,
FactoryOptions,
} from "../../../platform/background/service-factories/factory-options";
import {
stateProviderFactory,
StateProviderInitOptions,
} from "../../../platform/background/service-factories/state-provider.factory";
export type DomainSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
export function domainSettingsServiceFactory(
cache: { domainSettingsService?: DefaultDomainSettingsService } & CachedServices,
opts: DomainSettingsServiceInitOptions,
): Promise<DefaultDomainSettingsService> {
return factory(
cache,
"domainSettingsService",
opts,
async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)),
);
}

View File

@ -1,8 +1,8 @@
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { BrowserApi } from "../../platform/browser/browser-api"; import { BrowserApi } from "../../platform/browser/browser-api";
@ -73,7 +73,7 @@ export default class WebRequestBackground {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain, domain,
null, null,
UriMatchType.Host, UriMatchStrategy.Host,
); );
if (ciphers == null || ciphers.length !== 1) { if (ciphers == null || ciphers.length !== 1) {
error(); error();

View File

@ -6,7 +6,7 @@ import AutofillField from "../models/autofill-field";
import { WatchedForm } from "../models/watched-form"; import { WatchedForm } from "../models/watched-form";
import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar";
import { FormData } from "../services/abstractions/autofill.service"; import { FormData } from "../services/abstractions/autofill.service";
import { GlobalSettings, UserSettings } from "../types"; import { UserSettings } from "../types";
import { import {
getFromLocalStorage, getFromLocalStorage,
sendExtensionMessage, sendExtensionMessage,
@ -94,10 +94,11 @@ async function loadNotificationBar() {
"bgGetEnableChangedPasswordPrompt", "bgGetEnableChangedPasswordPrompt",
); );
const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt");
const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains");
let showNotificationBar = true; let showNotificationBar = true;
// Look up the active user id from storage // Look up the active user id from storage
const activeUserIdKey = "activeUserId"; const activeUserIdKey = "activeUserId";
const globalStorageKey = "global";
let activeUserId: string; let activeUserId: string;
const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey);
@ -109,9 +110,6 @@ async function loadNotificationBar() {
const userSettingsStorageValue = await getFromLocalStorage(activeUserId); const userSettingsStorageValue = await getFromLocalStorage(activeUserId);
if (userSettingsStorageValue[activeUserId]) { if (userSettingsStorageValue[activeUserId]) {
const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings;
const globalSettings: GlobalSettings = (await getFromLocalStorage(globalStorageKey))[
globalStorageKey
];
// Do not show the notification bar on the Bitwarden vault // Do not show the notification bar on the Bitwarden vault
// because they can add logins and change passwords there // because they can add logins and change passwords there
@ -122,8 +120,8 @@ async function loadNotificationBar() {
// show the notification bar on (for login detail collection or password change). // show the notification bar on (for login detail collection or password change).
// It is managed in the Settings > Excluded Domains page in the browser extension. // It is managed in the Settings > Excluded Domains page in the browser extension.
// Example: '{"bitwarden.com":null}' // Example: '{"bitwarden.com":null}'
const excludedDomainsDict = globalSettings.neverDomains;
if (!excludedDomainsDict || !(window.location.hostname in excludedDomainsDict)) { if (!excludedDomains || !(window.location.hostname in excludedDomains)) {
if (enableAddedLoginPrompt || enableChangedPasswordPrompt) { if (enableAddedLoginPrompt || enableChangedPasswordPrompt) {
// If the user has not disabled both notifications, then handle the initial page change (null -> actual page) // If the user has not disabled both notifications, then handle the initial page change (null -> actual page)
handlePageChange(); handlePageChange();

View File

@ -1,14 +1,16 @@
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { DialogService } from "@bitwarden/components"; import { DialogService } from "@bitwarden/components";
import { BrowserApi } from "../../../platform/browser/browser-api"; import { BrowserApi } from "../../../platform/browser/browser-api";
@ -28,16 +30,15 @@ export class AutofillComponent implements OnInit {
enableAutoFillOnPageLoad = false; enableAutoFillOnPageLoad = false;
autoFillOnPageLoadDefault = false; autoFillOnPageLoadDefault = false;
autoFillOnPageLoadOptions: any[]; autoFillOnPageLoadOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
autofillKeyboardHelperText: string; autofillKeyboardHelperText: string;
accountSwitcherEnabled = false; accountSwitcherEnabled = false;
constructor( constructor(
private stateService: StateService,
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private autofillService: AutofillService, private autofillService: AutofillService,
private dialogService: DialogService, private dialogService: DialogService,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
@ -61,12 +62,12 @@ export class AutofillComponent implements OnInit {
{ name: i18nService.t("autoFillOnPageLoadNo"), value: false }, { name: i18nService.t("autoFillOnPageLoadNo"), value: false },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.accountSwitcherEnabled = enableAccountSwitching(); this.accountSwitcherEnabled = enableAccountSwitching();
@ -94,8 +95,10 @@ export class AutofillComponent implements OnInit {
this.autofillSettingsService.autofillOnPageLoadDefault$, this.autofillSettingsService.autofillOnPageLoadDefault$,
); );
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); const command = await this.platformUtilsService.getAutofillKeyboardShortcut();
await this.setAutofillKeyboardHelperText(command); await this.setAutofillKeyboardHelperText(command);
@ -119,7 +122,7 @@ export class AutofillComponent implements OnInit {
} }
async saveDefaultUriMatch() { async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch); await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch);
} }
private async setAutofillKeyboardHelperText(command: string) { private async setAutofillKeyboardHelperText(command: string) {

View File

@ -1,4 +1,5 @@
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { UriMatchStrategySetting } from "@bitwarden/common/models/domain/domain-service";
import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import AutofillField from "../../models/autofill-field"; import AutofillField from "../../models/autofill-field";
@ -40,7 +41,7 @@ export interface GenerateFillScriptOptions {
allowTotpAutofill: boolean; allowTotpAutofill: boolean;
cipher: CipherView; cipher: CipherView;
tabUrl: string; tabUrl: string;
defaultUriMatch: UriMatchType; defaultUriMatch: UriMatchStrategySetting;
} }
export abstract class AutofillService { export abstract class AutofillService {

View File

@ -1,19 +1,25 @@
import { mock, mockReset } from "jest-mock-extended"; import { mock, mockReset } from "jest-mock-extended";
import { of } from "rxjs";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants";
import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsService } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { EventType } from "@bitwarden/common/enums";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/services/settings.service";
import { import {
FieldType, DefaultDomainSettingsService,
LinkedIdType, DomainSettingsService,
LoginLinkedId, } from "@bitwarden/common/autofill/services/domain-settings.service";
UriMatchType, import { EventType } from "@bitwarden/common/enums";
CipherType, import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
} from "@bitwarden/common/vault/enums"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { EventCollectionService } from "@bitwarden/common/services/event/event-collection.service";
import {
FakeStateProvider,
FakeAccountService,
mockAccountServiceWith,
} from "@bitwarden/common/spec";
import { UserId } from "@bitwarden/common/types/guid";
import { FieldType, LinkedIdType, LoginLinkedId, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -47,15 +53,24 @@ import {
import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants";
import AutofillService from "./autofill.service"; import AutofillService from "./autofill.service";
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
describe("AutofillService", () => { describe("AutofillService", () => {
let autofillService: AutofillService; let autofillService: AutofillService;
const cipherService = mock<CipherService>(); const cipherService = mock<CipherService>();
const stateService = mock<BrowserStateService>(); const stateService = mock<BrowserStateService>();
const autofillSettingsService = mock<AutofillSettingsService>(); const autofillSettingsService = mock<AutofillSettingsService>();
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
let domainSettingsService: DomainSettingsService;
const totpService = mock<TotpService>(); const totpService = mock<TotpService>();
const eventCollectionService = mock<EventCollectionService>(); const eventCollectionService = mock<EventCollectionService>();
const logService = mock<LogService>(); const logService = mock<LogService>();
const settingsService = mock<SettingsService>();
const userVerificationService = mock<UserVerificationService>(); const userVerificationService = mock<UserVerificationService>();
beforeEach(() => { beforeEach(() => {
@ -66,9 +81,12 @@ describe("AutofillService", () => {
totpService, totpService,
eventCollectionService, eventCollectionService,
logService, logService,
settingsService, domainSettingsService,
userVerificationService, userVerificationService,
); );
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
}); });
afterEach(() => { afterEach(() => {
@ -407,6 +425,8 @@ describe("AutofillService", () => {
autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true); autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true);
autofillOptions.cipher.login.username = "username"; autofillOptions.cipher.login.username = "username";
autofillOptions.cipher.login.password = "password"; autofillOptions.cipher.login.password = "password";
jest.spyOn(autofillService, "getDefaultUriMatchStrategy").mockResolvedValue(0);
}); });
describe("given a set of autofill options that are incomplete", () => { describe("given a set of autofill options that are incomplete", () => {
@ -468,7 +488,6 @@ describe("AutofillService", () => {
it("will autofill login data for a page", async () => { it("will autofill login data for a page", async () => {
jest.spyOn(stateService, "getCanAccessPremium"); jest.spyOn(stateService, "getCanAccessPremium");
jest.spyOn(stateService, "getDefaultUriMatch");
jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateFillScript");
jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript");
jest.spyOn(logService, "info"); jest.spyOn(logService, "info");
@ -479,7 +498,7 @@ describe("AutofillService", () => {
const currentAutofillPageDetails = autofillOptions.pageDetails[0]; const currentAutofillPageDetails = autofillOptions.pageDetails[0];
expect(stateService.getCanAccessPremium).toHaveBeenCalled(); expect(stateService.getCanAccessPremium).toHaveBeenCalled();
expect(stateService.getDefaultUriMatch).toHaveBeenCalled(); expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled();
expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( expect(autofillService["generateFillScript"]).toHaveBeenCalledWith(
currentAutofillPageDetails.details, currentAutofillPageDetails.details,
{ {
@ -1488,7 +1507,7 @@ describe("AutofillService", () => {
}; };
defaultLoginUriView = mock<LoginUriView>({ defaultLoginUriView = mock<LoginUriView>({
uri: "https://www.example.com", uri: "https://www.example.com",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
}); });
options = createGenerateFillScriptOptionsMock(); options = createGenerateFillScriptOptionsMock();
options.cipher.login = mock<LoginView>({ options.cipher.login = mock<LoginView>({
@ -1559,13 +1578,13 @@ describe("AutofillService", () => {
]); ]);
}); });
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => { it("skips adding any login uri views that have a UriMatchStrategySetting of Never to the list of saved urls", async () => {
const secondUriView = mock<LoginUriView>({ const secondUriView = mock<LoginUriView>({
uri: "https://www.second-example.com", uri: "https://www.second-example.com",
}); });
const thirdUriView = mock<LoginUriView>({ const thirdUriView = mock<LoginUriView>({
uri: "https://www.third-example.com", uri: "https://www.third-example.com",
match: UriMatchType.Never, match: UriMatchStrategy.Never,
}); });
options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView]; options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView];
@ -2752,31 +2771,32 @@ describe("AutofillService", () => {
}); });
describe("inUntrustedIframe", () => { describe("inUntrustedIframe", () => {
it("returns a false value if the passed pageUrl is equal to the options tabUrl", () => { it("returns a false value if the passed pageUrl is equal to the options tabUrl", async () => {
const pageUrl = "https://www.example.com"; const pageUrl = "https://www.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService, "getEquivalentDomains");
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).not.toHaveBeenCalled();
expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled(); expect(generateFillScriptOptions.cipher.login.matchesUri).not.toHaveBeenCalled();
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a false value if the passed pageUrl matches the domain of the tabUrl", () => { it("returns a false value if the passed pageUrl matches the domain of the tabUrl", async () => {
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.example.com"; const tabUrl = "https://www.example.com";
const equivalentDomains = new Set(["example.com"]); const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(true);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,
@ -2785,17 +2805,21 @@ describe("AutofillService", () => {
expect(result).toBe(false); expect(result).toBe(false);
}); });
it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", () => { it("returns a true value if the passed pageUrl does not match the domain of the tabUrl", async () => {
const equivalentDomains = new Set([
"ejemplo.es",
"example.co.uk",
"example.com",
"exampleapp.com",
]);
domainSettingsService.equivalentDomains$ = of([["not-example.com"]]);
const pageUrl = "https://subdomain.example.com"; const pageUrl = "https://subdomain.example.com";
const tabUrl = "https://www.not-example.com"; const tabUrl = "https://www.not-example.com";
const equivalentDomains = new Set(["not-example.com"]);
const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl });
generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false); generateFillScriptOptions.cipher.login.matchesUri = jest.fn().mockReturnValueOnce(false);
jest.spyOn(settingsService as any, "getEquivalentDomains").mockReturnValue(equivalentDomains);
const result = autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions); const result = await autofillService["inUntrustedIframe"](pageUrl, generateFillScriptOptions);
expect(settingsService.getEquivalentDomains).toHaveBeenCalledWith(pageUrl);
expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith( expect(generateFillScriptOptions.cipher.login.matchesUri).toHaveBeenCalledWith(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,15 +1,19 @@
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types"; import { InlineMenuVisibilitySetting } from "@bitwarden/common/autofill/types";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import {
UriMatchStrategySetting,
UriMatchStrategy,
} from "@bitwarden/common/models/domain/domain-service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { FieldType, UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { FieldType, CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { FieldView } from "@bitwarden/common/vault/models/view/field.view"; import { FieldView } from "@bitwarden/common/vault/models/view/field.view";
@ -48,7 +52,7 @@ export default class AutofillService implements AutofillServiceInterface {
private totpService: TotpService, private totpService: TotpService,
private eventCollectionService: EventCollectionService, private eventCollectionService: EventCollectionService,
private logService: LogService, private logService: LogService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private userVerificationService: UserVerificationService, private userVerificationService: UserVerificationService,
) {} ) {}
@ -215,6 +219,13 @@ export default class AutofillService implements AutofillServiceInterface {
return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$); return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$);
} }
/**
* Gets the default URI match strategy setting from the domain settings service.
*/
async getDefaultUriMatchStrategy(): Promise<UriMatchStrategySetting> {
return await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
}
/** /**
* Autofill a given tab with a given login item * Autofill a given tab with a given login item
* @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item * @param {AutoFillOptions} options Instructions about the autofill operation, including tab and login item
@ -229,7 +240,7 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: string | null = null; let totp: string | null = null;
const canAccessPremium = await this.stateService.getCanAccessPremium(); const canAccessPremium = await this.stateService.getCanAccessPremium();
const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain; const defaultUriMatch = await this.getDefaultUriMatchStrategy();
if (!canAccessPremium) { if (!canAccessPremium) {
options.cipher.login.totp = null; options.cipher.login.totp = null;
@ -579,9 +590,9 @@ export default class AutofillService implements AutofillServiceInterface {
let totp: AutofillField = null; let totp: AutofillField = null;
const login = options.cipher.login; const login = options.cipher.login;
fillScript.savedUrls = fillScript.savedUrls =
login?.uris?.filter((u) => u.match != UriMatchType.Never).map((u) => u.uri) ?? []; login?.uris?.filter((u) => u.match != UriMatchStrategy.Never).map((u) => u.uri) ?? [];
fillScript.untrustedIframe = this.inUntrustedIframe(pageDetails.url, options); fillScript.untrustedIframe = await this.inUntrustedIframe(pageDetails.url, options);
let passwordFields = AutofillService.loadPasswordFields( let passwordFields = AutofillService.loadPasswordFields(
pageDetails, pageDetails,
@ -1066,7 +1077,10 @@ export default class AutofillService implements AutofillServiceInterface {
* @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise * @returns {boolean} `true` if the iframe is untrusted and a warning should be shown, `false` otherwise
* @private * @private
*/ */
private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean { private async inUntrustedIframe(
pageUrl: string,
options: GenerateFillScriptOptions,
): Promise<boolean> {
// If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe // If the pageUrl (from the content script) matches the tabUrl (from the sender tab), we are not in an iframe
// This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway // This also avoids a false positive if no URI is saved and the user triggers auto-fill anyway
if (pageUrl === options.tabUrl) { if (pageUrl === options.tabUrl) {
@ -1076,7 +1090,9 @@ export default class AutofillService implements AutofillServiceInterface {
// Check the pageUrl against cipher URIs using the configured match detection. // Check the pageUrl against cipher URIs using the configured match detection.
// Remember: if we are in this function, the tabUrl already matches a saved URI for the login. // Remember: if we are in this function, the tabUrl already matches a saved URI for the login.
// We need to verify the pageUrl also matches. // We need to verify the pageUrl also matches.
const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(pageUrl),
);
const matchesUri = options.cipher.login.matchesUri( const matchesUri = options.cipher.login.matchesUri(
pageUrl, pageUrl,
equivalentDomains, equivalentDomains,

View File

@ -1,8 +1,9 @@
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { UriMatchType, CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@ -112,7 +113,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr
allowTotpAutofill: false, allowTotpAutofill: false,
cipher: mock<CipherView>(), cipher: mock<CipherView>(),
tabUrl: "https://jest-testing-website.com", tabUrl: "https://jest-testing-website.com",
defaultUriMatch: UriMatchType.Domain, defaultUriMatch: UriMatchStrategy.Domain,
...customFields, ...customFields,
}; };
} }

View File

@ -1,5 +1,4 @@
import { Region } from "@bitwarden/common/platform/abstractions/environment.service"; import { Region } from "@bitwarden/common/platform/abstractions/environment.service";
import { GlobalState } from "@bitwarden/common/platform/models/domain/global-state";
import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum"; import { VaultTimeoutAction } from "@bitwarden/common/src/enums/vault-timeout-action.enum";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@ -32,15 +31,10 @@ export type UserSettings = {
utcDate: string; utcDate: string;
version: string; version: string;
}; };
settings: {
equivalentDomains: string[][];
};
vaultTimeout: number; vaultTimeout: number;
vaultTimeoutAction: VaultTimeoutAction; vaultTimeoutAction: VaultTimeoutAction;
}; };
export type GlobalSettings = Pick<GlobalState, "neverDomains">;
/** /**
* A HTMLElement (usually a form element) with additional custom properties added by this script * A HTMLElement (usually a form element) with additional custom properties added by this script
*/ */

View File

@ -56,6 +56,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
DomainSettingsService,
DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { import {
UserNotificationSettingsService, UserNotificationSettingsService,
UserNotificationSettingsServiceAbstraction, UserNotificationSettingsServiceAbstraction,
@ -259,6 +263,7 @@ export default class MainBackground {
userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; userNotificationSettingsService: UserNotificationSettingsServiceAbstraction;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
badgeSettingsService: BadgeSettingsServiceAbstraction; badgeSettingsService: BadgeSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
systemService: SystemServiceAbstraction; systemService: SystemServiceAbstraction;
eventCollectionService: EventCollectionServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction;
eventUploadService: EventUploadServiceAbstraction; eventUploadService: EventUploadServiceAbstraction;
@ -466,6 +471,7 @@ export default class MainBackground {
this.appIdService, this.appIdService,
(expired: boolean) => this.logout(expired), (expired: boolean) => this.logout(expired),
); );
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.settingsService = new BrowserSettingsService(this.stateService); this.settingsService = new BrowserSettingsService(this.stateService);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
this.cipherFileUploadService = new CipherFileUploadService( this.cipherFileUploadService = new CipherFileUploadService(
@ -591,7 +597,7 @@ export default class MainBackground {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -678,7 +684,7 @@ export default class MainBackground {
this.providerService = new ProviderService(this.stateProvider); this.providerService = new ProviderService(this.stateProvider);
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -715,7 +721,7 @@ export default class MainBackground {
this.totpService, this.totpService,
this.eventCollectionService, this.eventCollectionService,
this.logService, this.logService,
this.settingsService, this.domainSettingsService,
this.userVerificationService, this.userVerificationService,
); );
this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService);
@ -779,6 +785,7 @@ export default class MainBackground {
this.authService, this.authService,
this.stateService, this.stateService,
this.vaultSettingsService, this.vaultSettingsService,
this.domainSettingsService,
this.logService, this.logService,
); );
@ -848,6 +855,7 @@ export default class MainBackground {
this.folderService, this.folderService,
this.stateService, this.stateService,
this.userNotificationSettingsService, this.userNotificationSettingsService,
this.domainSettingsService,
this.environmentService, this.environmentService,
this.logService, this.logService,
); );
@ -1081,7 +1089,6 @@ export default class MainBackground {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0), userId), this.syncService.setLastSync(new Date(0), userId),
this.cryptoService.clearKeys(userId), this.cryptoService.clearKeys(userId),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),

View File

@ -1,6 +1,8 @@
import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; import { Component, NgZone, OnDestroy, OnInit } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
@ -30,6 +32,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private i18nService: I18nService, private i18nService: I18nService,
private router: Router, private router: Router,
private broadcasterService: BroadcasterService, private broadcasterService: BroadcasterService,
@ -40,7 +43,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
const savedDomains = await this.stateService.getNeverDomains(); const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (savedDomains) { if (savedDomains) {
for (const uri of Object.keys(savedDomains)) { for (const uri of Object.keys(savedDomains)) {
this.excludedDomains.push({ uri: uri, showCurrentUris: false }); this.excludedDomains.push({ uri: uri, showCurrentUris: false });
@ -107,7 +110,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy {
} }
} }
await this.stateService.setNeverDomains(savedDomains); await this.domainSettingsService.setNeverDomains(savedDomains);
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
// eslint-disable-next-line @typescript-eslint/no-floating-promises // eslint-disable-next-line @typescript-eslint/no-floating-promises
this.router.navigate(["/tabs/settings"]); this.router.navigate(["/tabs/settings"]);

View File

@ -5,14 +5,18 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { SettingsService } from "@bitwarden/common/abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service"; import { UserNotificationSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/user-notification-settings.service";
import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types"; import { ClearClipboardDelaySetting } from "@bitwarden/common/autofill/types";
import {
UriMatchStrategy,
UriMatchStrategySetting,
} from "@bitwarden/common/models/domain/domain-service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { ThemeType } from "@bitwarden/common/platform/enums"; import { ThemeType } from "@bitwarden/common/platform/enums";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { UriMatchType } from "@bitwarden/common/vault/enums";
import { enableAccountSwitching } from "../../platform/flags"; import { enableAccountSwitching } from "../../platform/flags";
@ -36,7 +40,7 @@ export class OptionsComponent implements OnInit {
showClearClipboard = true; showClearClipboard = true;
theme: ThemeType; theme: ThemeType;
themeOptions: any[]; themeOptions: any[];
defaultUriMatch = UriMatchType.Domain; defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain;
uriMatchOptions: any[]; uriMatchOptions: any[];
clearClipboard: ClearClipboardDelaySetting; clearClipboard: ClearClipboardDelaySetting;
clearClipboardOptions: any[]; clearClipboardOptions: any[];
@ -50,6 +54,7 @@ export class OptionsComponent implements OnInit {
private stateService: StateService, private stateService: StateService,
private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction,
private autofillSettingsService: AutofillSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction,
private domainSettingsService: DomainSettingsService,
private badgeSettingsService: BadgeSettingsServiceAbstraction, private badgeSettingsService: BadgeSettingsServiceAbstraction,
i18nService: I18nService, i18nService: I18nService,
private themingService: AbstractThemingService, private themingService: AbstractThemingService,
@ -64,12 +69,12 @@ export class OptionsComponent implements OnInit {
{ name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark },
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.clearClipboardOptions = [ this.clearClipboardOptions = [
{ name: i18nService.t("never"), value: null }, { name: i18nService.t("never"), value: null },
@ -122,8 +127,10 @@ export class OptionsComponent implements OnInit {
this.theme = await this.stateService.getTheme(); this.theme = await this.stateService.getTheme();
const defaultUriMatch = await this.stateService.getDefaultUriMatch(); const defaultUriMatch = await firstValueFrom(
this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; this.domainSettingsService.defaultUriMatchStrategy$,
);
this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch;
this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$);
} }
@ -182,10 +189,6 @@ export class OptionsComponent implements OnInit {
await this.themingService.updateConfiguredTheme(this.theme); await this.themingService.updateConfiguredTheme(this.theme);
} }
async saveDefaultUriMatch() {
await this.stateService.setDefaultUriMatch(this.defaultUriMatch);
}
async saveClearClipboard() { async saveClearClipboard() {
await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard);
} }

View File

@ -1,15 +1,11 @@
import { BehaviorSubject } from "rxjs"; import { BehaviorSubject } from "rxjs";
import { AccountSettingsSettings } from "@bitwarden/common/platform/models/domain/account";
import { SettingsService } from "@bitwarden/common/services/settings.service"; import { SettingsService } from "@bitwarden/common/services/settings.service";
import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable";
@browserSession @browserSession
export class BrowserSettingsService extends SettingsService { export class BrowserSettingsService extends SettingsService {
@sessionSync({ initializer: (obj: string[][]) => obj })
protected _settings: BehaviorSubject<AccountSettingsSettings>;
@sessionSync({ initializer: (b: boolean) => b }) @sessionSync({ initializer: (b: boolean) => b })
protected _disableFavicon: BehaviorSubject<boolean>; protected _disableFavicon: BehaviorSubject<boolean>;
} }

View File

@ -5,6 +5,10 @@ import {
AutofillSettingsServiceInitOptions, AutofillSettingsServiceInitOptions,
autofillSettingsServiceFactory, autofillSettingsServiceFactory,
} from "../../../autofill/background/service_factories/autofill-settings-service.factory"; } from "../../../autofill/background/service_factories/autofill-settings-service.factory";
import {
DomainSettingsServiceInitOptions,
domainSettingsServiceFactory,
} from "../../../autofill/background/service_factories/domain-settings-service.factory";
import { import {
CipherFileUploadServiceInitOptions, CipherFileUploadServiceInitOptions,
cipherFileUploadServiceFactory, cipherFileUploadServiceFactory,
@ -13,10 +17,6 @@ import {
searchServiceFactory, searchServiceFactory,
SearchServiceInitOptions, SearchServiceInitOptions,
} from "../../../background/service-factories/search-service.factory"; } from "../../../background/service-factories/search-service.factory";
import {
SettingsServiceInitOptions,
settingsServiceFactory,
} from "../../../background/service-factories/settings-service.factory";
import { import {
apiServiceFactory, apiServiceFactory,
ApiServiceInitOptions, ApiServiceInitOptions,
@ -51,13 +51,13 @@ type CipherServiceFactoryOptions = FactoryOptions;
export type CipherServiceInitOptions = CipherServiceFactoryOptions & export type CipherServiceInitOptions = CipherServiceFactoryOptions &
CryptoServiceInitOptions & CryptoServiceInitOptions &
SettingsServiceInitOptions &
ApiServiceInitOptions & ApiServiceInitOptions &
CipherFileUploadServiceInitOptions & CipherFileUploadServiceInitOptions &
I18nServiceInitOptions & I18nServiceInitOptions &
SearchServiceInitOptions & SearchServiceInitOptions &
StateServiceInitOptions & StateServiceInitOptions &
AutofillSettingsServiceInitOptions & AutofillSettingsServiceInitOptions &
DomainSettingsServiceInitOptions &
EncryptServiceInitOptions & EncryptServiceInitOptions &
ConfigServiceInitOptions; ConfigServiceInitOptions;
@ -72,7 +72,7 @@ export function cipherServiceFactory(
async () => async () =>
new CipherService( new CipherService(
await cryptoServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts),
await settingsServiceFactory(cache, opts), await domainSettingsServiceFactory(cache, opts),
await apiServiceFactory(cache, opts), await apiServiceFactory(cache, opts),
await i18nServiceFactory(cache, opts), await i18nServiceFactory(cache, opts),
await searchServiceFactory(cache, opts), await searchServiceFactory(cache, opts),

View File

@ -3,6 +3,7 @@ import { ConnectedPosition } from "@angular/cdk/overlay";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
@ -52,6 +53,7 @@ export class Fido2UseBrowserLinkComponent {
constructor( constructor(
private stateService: StateService, private stateService: StateService,
private domainSettingsService: DomainSettingsService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private i18nService: I18nService, private i18nService: I18nService,
) {} ) {}
@ -89,7 +91,7 @@ export class Fido2UseBrowserLinkComponent {
* @param uri - The domain uri to exclude from future FIDO2 prompts. * @param uri - The domain uri to exclude from future FIDO2 prompts.
*/ */
private async handleDomainExclusion(uri: string) { private async handleDomainExclusion(uri: string) {
const exisitingDomains = await this.stateService.getNeverDomains(); const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const validDomain = Utils.getHostname(uri); const validDomain = Utils.getHostname(uri);
const savedDomains: { [name: string]: unknown } = { const savedDomains: { [name: string]: unknown } = {
@ -97,9 +99,7 @@ export class Fido2UseBrowserLinkComponent {
}; };
savedDomains[validDomain] = null; savedDomains[validDomain] = null;
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. await this.domainSettingsService.setNeverDomains(savedDomains);
// eslint-disable-next-line @typescript-eslint/no-floating-promises
this.stateService.setNeverDomains(savedDomains);
this.platformUtilsService.showToast( this.platformUtilsService.showToast(
"success", "success",

View File

@ -5,6 +5,7 @@ import {
combineLatest, combineLatest,
concatMap, concatMap,
filter, filter,
firstValueFrom,
map, map,
Observable, Observable,
Subject, Subject,
@ -13,7 +14,7 @@ import {
} from "rxjs"; } from "rxjs";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@ -72,7 +73,7 @@ export class Fido2Component implements OnInit, OnDestroy {
private cipherService: CipherService, private cipherService: CipherService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private searchService: SearchService, private searchService: SearchService,
private logService: LogService, private logService: LogService,
private dialogService: DialogService, private dialogService: DialogService,
@ -133,7 +134,9 @@ export class Fido2Component implements OnInit, OnDestroy {
concatMap(async (message) => { concatMap(async (message) => {
switch (message.type) { switch (message.type) {
case "ConfirmNewCredentialRequest": { case "ConfirmNewCredentialRequest": {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.ciphers = (await this.cipherService.getAllDecrypted()).filter( this.ciphers = (await this.cipherService.getAllDecrypted()).filter(
(cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
@ -317,7 +320,9 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers, this.ciphers,
); );
} else { } else {
const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(this.url),
);
this.displayedCiphers = this.ciphers.filter((cipher) => this.displayedCiphers = this.ciphers.filter((cipher) =>
cipher.login.matchesUri(this.url, equivalentDomains), cipher.login.matchesUri(this.url, equivalentDomains),
); );

View File

@ -35,6 +35,10 @@ import { TwoFactorService } from "@bitwarden/common/auth/services/two-factor.ser
import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service"; import { UserVerificationApiService } from "@bitwarden/common/auth/services/user-verification/user-verification-api.service";
import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service";
import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-settings.service";
import {
DefaultDomainSettingsService,
DomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { ClientType } from "@bitwarden/common/enums"; import { ClientType } from "@bitwarden/common/enums";
import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction";
import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service";
@ -190,6 +194,7 @@ export class Main {
pinCryptoService: PinCryptoServiceAbstraction; pinCryptoService: PinCryptoServiceAbstraction;
stateService: StateService; stateService: StateService;
autofillSettingsService: AutofillSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction;
domainSettingsService: DomainSettingsService;
organizationService: OrganizationService; organizationService: OrganizationService;
providerService: ProviderService; providerService: ProviderService;
twoFactorService: TwoFactorService; twoFactorService: TwoFactorService;
@ -358,6 +363,7 @@ export class Main {
this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.containerService = new ContainerService(this.cryptoService, this.encryptService);
this.settingsService = new SettingsService(this.stateService); this.settingsService = new SettingsService(this.stateService);
this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider);
this.fileUploadService = new FileUploadService(this.logService); this.fileUploadService = new FileUploadService(this.logService);
@ -481,7 +487,7 @@ export class Main {
this.cipherService = new CipherService( this.cipherService = new CipherService(
this.cryptoService, this.cryptoService,
this.settingsService, this.domainSettingsService,
this.apiService, this.apiService,
this.i18nService, this.i18nService,
this.searchService, this.searchService,
@ -551,7 +557,7 @@ export class Main {
this.syncService = new SyncService( this.syncService = new SyncService(
this.apiService, this.apiService,
this.settingsService, this.domainSettingsService,
this.folderService, this.folderService,
this.cipherService, this.cipherService,
this.cryptoService, this.cryptoService,
@ -647,7 +653,6 @@ export class Main {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId as UserId), this.collectionService.clear(userId as UserId),

View File

@ -577,7 +577,6 @@ export class AppComponent implements OnInit, OnDestroy {
await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.eventUploadService.uploadEvents(userBeingLoggedOut);
await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut);
await this.cryptoService.clearKeys(userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut);
await this.settingsService.clear(userBeingLoggedOut);
await this.cipherService.clear(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut);
await this.folderService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut);
await this.collectionService.clear(userBeingLoggedOut); await this.collectionService.clear(userBeingLoggedOut);

View File

@ -275,7 +275,6 @@ export class AppComponent implements OnDestroy, OnInit {
await Promise.all([ await Promise.all([
this.syncService.setLastSync(new Date(0)), this.syncService.setLastSync(new Date(0)),
this.cryptoService.clearKeys(), this.cryptoService.clearKeys(),
this.settingsService.clear(userId),
this.cipherService.clear(userId), this.cipherService.clear(userId),
this.folderService.clear(userId), this.folderService.clear(userId),
this.collectionService.clear(userId), this.collectionService.clear(userId),

View File

@ -90,6 +90,10 @@ import {
BadgeSettingsServiceAbstraction, BadgeSettingsServiceAbstraction,
BadgeSettingsService, BadgeSettingsService,
} from "@bitwarden/common/autofill/services/badge-settings.service"; } from "@bitwarden/common/autofill/services/badge-settings.service";
import {
DomainSettingsService,
DefaultDomainSettingsService,
} from "@bitwarden/common/autofill/services/domain-settings.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billilng-api.service.abstraction";
import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service";
import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction";
@ -351,13 +355,14 @@ import { ModalService } from "./modal.service";
searchService: SearchServiceAbstraction, searchService: SearchServiceAbstraction,
stateService: StateServiceAbstraction, stateService: StateServiceAbstraction,
autofillSettingsService: AutofillSettingsServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction,
domainSettingsService: DomainSettingsService,
encryptService: EncryptService, encryptService: EncryptService,
fileUploadService: CipherFileUploadServiceAbstraction, fileUploadService: CipherFileUploadServiceAbstraction,
configService: ConfigServiceAbstraction, configService: ConfigServiceAbstraction,
) => ) =>
new CipherService( new CipherService(
cryptoService, cryptoService,
settingsService, domainSettingsService,
apiService, apiService,
i18nService, i18nService,
searchService, searchService,
@ -962,6 +967,11 @@ import { ModalService } from "./modal.service";
useClass: BadgeSettingsService, useClass: BadgeSettingsService,
deps: [StateProvider], deps: [StateProvider],
}, },
{
provide: DomainSettingsService,
useClass: DefaultDomainSettingsService,
deps: [StateProvider],
},
{ {
provide: BiometricStateService, provide: BiometricStateService,
useClass: DefaultBiometricStateService, useClass: DefaultBiometricStateService,

View File

@ -13,6 +13,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
@ -24,7 +25,7 @@ import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.s
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType, SecureNoteType, UriMatchType } from "@bitwarden/common/vault/enums"; import { CipherType, SecureNoteType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CardView } from "@bitwarden/common/vault/models/view/card.view";
@ -164,12 +165,12 @@ export class AddEditComponent implements OnInit, OnDestroy {
]; ];
this.uriMatchOptions = [ this.uriMatchOptions = [
{ name: i18nService.t("defaultMatchDetection"), value: null }, { name: i18nService.t("defaultMatchDetection"), value: null },
{ name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain },
{ name: i18nService.t("host"), value: UriMatchType.Host }, { name: i18nService.t("host"), value: UriMatchStrategy.Host },
{ name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith },
{ name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression },
{ name: i18nService.t("exact"), value: UriMatchType.Exact }, { name: i18nService.t("exact"), value: UriMatchStrategy.Exact },
{ name: i18nService.t("never"), value: UriMatchType.Never }, { name: i18nService.t("never"), value: UriMatchStrategy.Never },
]; ];
this.autofillOnPageLoadOptions = [ this.autofillOnPageLoadOptions = [
{ name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null }, { name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null },

View File

@ -1,14 +1,8 @@
import { Observable } from "rxjs"; import { Observable } from "rxjs";
import { AccountSettingsSettings } from "../platform/models/domain/account";
export abstract class SettingsService { export abstract class SettingsService {
settings$: Observable<AccountSettingsSettings>;
disableFavicon$: Observable<boolean>; disableFavicon$: Observable<boolean>;
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
getEquivalentDomains: (url: string) => Set<string>;
setDisableFavicon: (value: boolean) => Promise<any>; setDisableFavicon: (value: boolean) => Promise<any>;
getDisableFavicon: () => boolean; getDisableFavicon: () => boolean;
clear: (userId?: string) => Promise<void>;
} }

View File

@ -0,0 +1,53 @@
import { firstValueFrom, of } from "rxjs";
import { FakeStateProvider, FakeAccountService, mockAccountServiceWith } from "../../../spec";
import { Utils } from "../../platform/misc/utils";
import { UserId } from "../../types/guid";
import { DefaultDomainSettingsService, DomainSettingsService } from "./domain-settings.service";
describe("DefaultDomainSettingsService", () => {
let domainSettingsService: DomainSettingsService;
const mockUserId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider);
jest.spyOn(domainSettingsService, "getUrlEquivalentDomains");
domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains);
});
describe("getUrlEquivalentDomains", () => {
it("returns all equivalent domains for a URL", async () => {
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
const actual = await firstValueFrom(
domainSettingsService.getUrlEquivalentDomains("example.co.uk"),
);
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("example.co.uk");
expect(actual).toEqual(expected);
});
it("returns an empty set if there are no equivalent domains", async () => {
const actual = await firstValueFrom(domainSettingsService.getUrlEquivalentDomains("asdf"));
expect(domainSettingsService.getUrlEquivalentDomains).toHaveBeenCalledWith("asdf");
expect(actual).toEqual(new Set());
});
});
});

View File

@ -0,0 +1,97 @@
import { map, Observable } from "rxjs";
import {
NeverDomains,
EquivalentDomains,
UriMatchStrategySetting,
UriMatchStrategy,
} from "../../models/domain/domain-service";
import { Utils } from "../../platform/misc/utils";
import {
DOMAIN_SETTINGS_DISK,
ActiveUserState,
GlobalState,
KeyDefinition,
StateProvider,
UserKeyDefinition,
} from "../../platform/state";
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
deserializer: (value: NeverDomains) => value ?? null,
});
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
deserializer: (value: EquivalentDomains) => value ?? null,
clearOn: ["logout"],
});
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
DOMAIN_SETTINGS_DISK,
"defaultUriMatchStrategy",
{
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
},
);
export abstract class DomainSettingsService {
neverDomains$: Observable<NeverDomains>;
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
equivalentDomains$: Observable<EquivalentDomains>;
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
}
export class DefaultDomainSettingsService implements DomainSettingsService {
private neverDomainsState: GlobalState<NeverDomains>;
readonly neverDomains$: Observable<NeverDomains>;
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
readonly equivalentDomains$: Observable<EquivalentDomains>;
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
constructor(private stateProvider: StateProvider) {
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
this.defaultUriMatchStrategyState = this.stateProvider.getActive(DEFAULT_URI_MATCH_STRATEGY);
this.defaultUriMatchStrategy$ = this.defaultUriMatchStrategyState.state$.pipe(
map((x) => x ?? UriMatchStrategy.Domain),
);
}
async setNeverDomains(newValue: NeverDomains): Promise<void> {
await this.neverDomainsState.update(() => newValue);
}
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
await this.equivalentDomainsState.update(() => newValue);
}
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {
await this.defaultUriMatchStrategyState.update(() => newValue);
}
getUrlEquivalentDomains(url: string): Observable<Set<string>> {
const domains$ = this.equivalentDomains$.pipe(
map((equivalentDomains) => {
const domain = Utils.getDomain(url);
if (domain == null || equivalentDomains == null) {
return new Set() as Set<string>;
}
const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat();
return new Set(equivalents);
}),
);
return domains$;
}
}

View File

@ -0,0 +1,24 @@
/*
See full documentation at:
https://bitwarden.com/help/uri-match-detection/#match-detection-options
Domain: "the top-level domain and second-level domain of the URI match the detected resource",
Host: "the hostname and (if specified) port of the URI matches the detected resource",
StartsWith: "the detected resource starts with the URI, regardless of what follows it",
Exact: "the URI matches the detected resource exactly",
RegularExpression: "the detected resource matches a specified regular expression",
Never: "never offer auto-fill for the item",
*/
export const UriMatchStrategy = {
Domain: 0,
Host: 1,
StartsWith: 2,
Exact: 3,
RegularExpression: 4,
Never: 5,
} as const;
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
export type NeverDomains = { [id: string]: unknown };
export type EquivalentDomains = string[][];

View File

@ -1,5 +1,5 @@
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { UriMatchType } from "../../vault/enums";
import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri";
import { LoginUriView } from "../../vault/models/view/login-uri.view"; import { LoginUriView } from "../../vault/models/view/login-uri.view";
@ -26,7 +26,7 @@ export class LoginUriExport {
uri: string; uri: string;
uriChecksum: string | undefined; uriChecksum: string | undefined;
match: UriMatchType = null; match: UriMatchStrategySetting = null;
constructor(o?: LoginUriView | LoginUriDomain) { constructor(o?: LoginUriView | LoginUriDomain) {
if (o == null) { if (o == null) {

View File

@ -14,18 +14,13 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view"; import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key"; import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data"; import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info";
import { KdfType, ThemeType } from "../enums"; import { KdfType, ThemeType } from "../enums";
import { ServerConfigData } from "../models/data/server-config.data"; import { ServerConfigData } from "../models/data/server-config.data";
import { import { Account, AccountDecryptionOptions } from "../models/domain/account";
Account,
AccountDecryptionOptions,
AccountSettingsSettings,
} from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { StorageOptions } from "../models/domain/storage-options"; import { StorageOptions } from "../models/domain/storage-options";
import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key";
@ -187,8 +182,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>; setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
/** /**
* @deprecated Do not call this, use SettingsService * @deprecated Do not call this, use SettingsService
*/ */
@ -275,8 +268,6 @@ export abstract class StateService<T extends Account = Account> {
* @deprecated Do not call this directly, use SendService * @deprecated Do not call this directly, use SendService
*/ */
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>; setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>; getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>; setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>; getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
@ -310,8 +301,6 @@ export abstract class StateService<T extends Account = Account> {
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>; setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>; getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>; setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>; getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
@ -353,14 +342,6 @@ export abstract class StateService<T extends Account = Account> {
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>; setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
getSecurityStamp: (options?: StorageOptions) => Promise<string>; getSecurityStamp: (options?: StorageOptions) => Promise<string>;
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>; setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
getSettings: (options?: StorageOptions) => Promise<AccountSettingsSettings>;
/**
* @deprecated Do not call this directly, use SettingsService
*/
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
getTheme: (options?: StorageOptions) => Promise<ThemeType>; getTheme: (options?: StorageOptions) => Promise<ThemeType>;
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>; setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
getTwoFactorToken: (options?: StorageOptions) => Promise<string>; getTwoFactorToken: (options?: StorageOptions) => Promise<string>;

View File

@ -7,6 +7,7 @@ import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/us
import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option";
import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response";
import { EventData } from "../../../models/data/event.data"; import { EventData } from "../../../models/data/event.data";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { GeneratorOptions } from "../../../tools/generator/generator-options";
import { import {
GeneratedPasswordHistory, GeneratedPasswordHistory,
@ -17,7 +18,6 @@ import { SendData } from "../../../tools/send/models/data/send.data";
import { SendView } from "../../../tools/send/models/view/send.view"; import { SendView } from "../../../tools/send/models/view/send.view";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { MasterKey } from "../../../types/key"; import { MasterKey } from "../../../types/key";
import { UriMatchType } from "../../../vault/enums";
import { CipherData } from "../../../vault/models/data/cipher.data"; import { CipherData } from "../../../vault/models/data/cipher.data";
import { CipherView } from "../../../vault/models/view/cipher.view"; import { CipherView } from "../../../vault/models/view/cipher.view";
import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info";
@ -196,13 +196,12 @@ export class AccountProfile {
export class AccountSettings { export class AccountSettings {
autoConfirmFingerPrints?: boolean; autoConfirmFingerPrints?: boolean;
defaultUriMatch?: UriMatchType; defaultUriMatch?: UriMatchStrategySetting;
disableGa?: boolean; disableGa?: boolean;
dontShowCardsCurrentTab?: boolean; dontShowCardsCurrentTab?: boolean;
dontShowIdentitiesCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean;
enableAlwaysOnTop?: boolean; enableAlwaysOnTop?: boolean;
enableBiometric?: boolean; enableBiometric?: boolean;
equivalentDomains?: any;
minimizeOnCopyToClipboard?: boolean; minimizeOnCopyToClipboard?: boolean;
passwordGenerationOptions?: PasswordGeneratorOptions; passwordGenerationOptions?: PasswordGeneratorOptions;
usernameGenerationOptions?: UsernameGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions;
@ -210,7 +209,6 @@ export class AccountSettings {
pinKeyEncryptedUserKey?: EncryptedString; pinKeyEncryptedUserKey?: EncryptedString;
pinKeyEncryptedUserKeyEphemeral?: EncryptedString; pinKeyEncryptedUserKeyEphemeral?: EncryptedString;
protectedPin?: string; protectedPin?: string;
settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly
vaultTimeout?: number; vaultTimeout?: number;
vaultTimeoutAction?: string = "lock"; vaultTimeoutAction?: string = "lock";
serverConfig?: ServerConfigData; serverConfig?: ServerConfigData;
@ -236,10 +234,6 @@ export class AccountSettings {
} }
} }
export type AccountSettingsSettings = {
equivalentDomains?: string[][];
};
export class AccountTokens { export class AccountTokens {
accessToken?: string; accessToken?: string;
refreshToken?: string; refreshToken?: string;

View File

@ -25,6 +25,5 @@ export class GlobalState {
enableBrowserIntegration?: boolean; enableBrowserIntegration?: boolean;
enableBrowserIntegrationFingerprint?: boolean; enableBrowserIntegrationFingerprint?: boolean;
enableDuckDuckGoBrowserIntegration?: boolean; enableDuckDuckGoBrowserIntegration?: boolean;
neverDomains?: { [id: string]: unknown };
deepLinkRedirectUrl?: string; deepLinkRedirectUrl?: string;
} }

View File

@ -18,7 +18,6 @@ import { SendData } from "../../tools/send/models/data/send.data";
import { SendView } from "../../tools/send/models/view/send.view"; import { SendView } from "../../tools/send/models/view/send.view";
import { UserId } from "../../types/guid"; import { UserId } from "../../types/guid";
import { DeviceKey, MasterKey } from "../../types/key"; import { DeviceKey, MasterKey } from "../../types/key";
import { UriMatchType } from "../../vault/enums";
import { CipherData } from "../../vault/models/data/cipher.data"; import { CipherData } from "../../vault/models/data/cipher.data";
import { LocalData } from "../../vault/models/data/local.data"; import { LocalData } from "../../vault/models/data/local.data";
import { CipherView } from "../../vault/models/view/cipher.view"; import { CipherView } from "../../vault/models/view/cipher.view";
@ -42,7 +41,6 @@ import {
AccountData, AccountData,
AccountDecryptionOptions, AccountDecryptionOptions,
AccountSettings, AccountSettings,
AccountSettingsSettings,
} from "../models/domain/account"; } from "../models/domain/account";
import { EncString } from "../models/domain/enc-string"; import { EncString } from "../models/domain/enc-string";
import { GlobalState } from "../models/domain/global-state"; import { GlobalState } from "../models/domain/global-state";
@ -815,23 +813,6 @@ export class StateService<
); );
} }
async getDefaultUriMatch(options?: StorageOptions): Promise<UriMatchType> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.defaultUriMatch;
}
async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.defaultUriMatch = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getDisableFavicon(options?: StorageOptions): Promise<boolean> { async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
return ( return (
( (
@ -1333,23 +1314,6 @@ export class StateService<
); );
} }
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.settings?.equivalentDomains;
}
async setEquivalentDomains(value: string, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
account.settings.equivalentDomains = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
@withPrototypeForArrayMembers(EventData) @withPrototypeForArrayMembers(EventData)
async getEventCollection(options?: StorageOptions): Promise<EventData[]> { async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
return ( return (
@ -1609,23 +1573,6 @@ export class StateService<
); );
} }
async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> {
return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
)?.neverDomains;
}
async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise<void> {
const globals = await this.getGlobals(
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
globals.neverDomains = value;
await this.saveGlobals(
globals,
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
);
}
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> { async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
return ( return (
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
@ -1807,23 +1754,6 @@ export class StateService<
); );
} }
async getSettings(options?: StorageOptions): Promise<AccountSettingsSettings> {
return (
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
)?.settings?.settings;
}
async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise<void> {
const account = await this.getAccount(
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
account.settings.settings = value;
await this.saveAccount(
account,
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
);
}
async getTheme(options?: StorageOptions): Promise<ThemeType> { async getTheme(options?: StorageOptions): Promise<ThemeType> {
return ( return (
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))

View File

@ -39,6 +39,8 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition(
// Billing // Billing
export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk");
export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk"); export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk");
export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", { export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", {
web: "disk-local", web: "disk-local",

View File

@ -1,10 +1,11 @@
import * as lunr from "lunr"; import * as lunr from "lunr";
import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service";
import { UriMatchStrategy } from "../models/domain/domain-service";
import { I18nService } from "../platform/abstractions/i18n.service"; import { I18nService } from "../platform/abstractions/i18n.service";
import { LogService } from "../platform/abstractions/log.service"; import { LogService } from "../platform/abstractions/log.service";
import { SendView } from "../tools/send/models/view/send.view"; import { SendView } from "../tools/send/models/view/send.view";
import { FieldType, UriMatchType } from "../vault/enums"; import { FieldType } from "../vault/enums";
import { CipherType } from "../vault/enums/cipher-type"; import { CipherType } from "../vault/enums/cipher-type";
import { CipherView } from "../vault/models/view/cipher.view"; import { CipherView } from "../vault/models/view/cipher.view";
@ -288,7 +289,7 @@ export class SearchService implements SearchServiceAbstraction {
return; return;
} }
let uri = u.uri; let uri = u.uri;
if (u.match !== UriMatchType.RegularExpression) { if (u.match !== UriMatchStrategy.RegularExpression) {
const protocolIndex = uri.indexOf("://"); const protocolIndex = uri.indexOf("://");
if (protocolIndex > -1) { if (protocolIndex > -1) {
uri = uri.substr(protocolIndex + 3); uri = uri.substr(protocolIndex + 3);

View File

@ -1,83 +0,0 @@
import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, firstValueFrom } from "rxjs";
import { CryptoService } from "../platform/abstractions/crypto.service";
import { EncryptService } from "../platform/abstractions/encrypt.service";
import { StateService } from "../platform/abstractions/state.service";
import { ContainerService } from "../platform/services/container.service";
import { SettingsService } from "./settings.service";
describe("SettingsService", () => {
let settingsService: SettingsService;
let cryptoService: MockProxy<CryptoService>;
let encryptService: MockProxy<EncryptService>;
let stateService: MockProxy<StateService>;
let activeAccount: BehaviorSubject<string>;
let activeAccountUnlocked: BehaviorSubject<boolean>;
const mockEquivalentDomains = [
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
["example.co.uk", "exampleapp.co.uk"],
];
beforeEach(() => {
cryptoService = mock<CryptoService>();
encryptService = mock<EncryptService>();
stateService = mock<StateService>();
activeAccount = new BehaviorSubject("123");
activeAccountUnlocked = new BehaviorSubject(true);
stateService.getSettings.mockResolvedValue({ equivalentDomains: mockEquivalentDomains });
stateService.activeAccount$ = activeAccount;
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
settingsService = new SettingsService(stateService);
});
afterEach(() => {
activeAccount.complete();
activeAccountUnlocked.complete();
});
describe("getEquivalentDomains", () => {
it("returns all equivalent domains for a URL", async () => {
const actual = settingsService.getEquivalentDomains("example.co.uk");
const expected = new Set([
"example.com",
"exampleapp.com",
"example.co.uk",
"ejemplo.es",
"exampleapp.co.uk",
]);
expect(actual).toEqual(expected);
});
it("returns an empty set if there are no equivalent domains", () => {
const actual = settingsService.getEquivalentDomains("asdf");
expect(actual).toEqual(new Set());
});
});
it("setEquivalentDomains", async () => {
await settingsService.setEquivalentDomains([["test2"], ["domains2"]]);
expect(stateService.setSettings).toBeCalledTimes(1);
expect((await firstValueFrom(settingsService.settings$)).equivalentDomains).toEqual([
["test2"],
["domains2"],
]);
});
it("clear", async () => {
await settingsService.clear();
expect(stateService.setSettings).toBeCalledTimes(1);
expect(await firstValueFrom(settingsService.settings$)).toEqual({});
});
});

View File

@ -3,13 +3,10 @@ import { BehaviorSubject, concatMap } from "rxjs";
import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service"; import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service";
import { StateService } from "../platform/abstractions/state.service"; import { StateService } from "../platform/abstractions/state.service";
import { Utils } from "../platform/misc/utils"; import { Utils } from "../platform/misc/utils";
import { AccountSettingsSettings } from "../platform/models/domain/account";
export class SettingsService implements SettingsServiceAbstraction { export class SettingsService implements SettingsServiceAbstraction {
protected _settings: BehaviorSubject<AccountSettingsSettings> = new BehaviorSubject({});
protected _disableFavicon = new BehaviorSubject<boolean>(null); protected _disableFavicon = new BehaviorSubject<boolean>(null);
settings$ = this._settings.asObservable();
disableFavicon$ = this._disableFavicon.asObservable(); disableFavicon$ = this._disableFavicon.asObservable();
constructor(private stateService: StateService) { constructor(private stateService: StateService) {
@ -21,50 +18,17 @@ export class SettingsService implements SettingsServiceAbstraction {
} }
if (!unlocked) { if (!unlocked) {
this._settings.next({});
return; return;
} }
const data = await this.stateService.getSettings();
const disableFavicon = await this.stateService.getDisableFavicon(); const disableFavicon = await this.stateService.getDisableFavicon();
this._settings.next(data);
this._disableFavicon.next(disableFavicon); this._disableFavicon.next(disableFavicon);
}), }),
) )
.subscribe(); .subscribe();
} }
async setEquivalentDomains(equivalentDomains: string[][]): Promise<void> {
const settings = this._settings.getValue() ?? {};
settings.equivalentDomains = equivalentDomains;
this._settings.next(settings);
await this.stateService.setSettings(settings);
}
getEquivalentDomains(url: string): Set<string> {
const domain = Utils.getDomain(url);
if (domain == null) {
return new Set();
}
const settings = this._settings.getValue();
let result: string[] = [];
if (settings?.equivalentDomains != null) {
settings.equivalentDomains
.filter((ed) => ed.length > 0 && ed.includes(domain))
.forEach((ed) => {
result = result.concat(ed);
});
}
return new Set(result);
}
async setDisableFavicon(value: boolean) { async setDisableFavicon(value: boolean) {
this._disableFavicon.next(value); this._disableFavicon.next(value);
await this.stateService.setDisableFavicon(value); await this.stateService.setDisableFavicon(value);
@ -73,12 +37,4 @@ export class SettingsService implements SettingsServiceAbstraction {
getDisableFavicon(): boolean { getDisableFavicon(): boolean {
return this._disableFavicon.getValue(); return this._disableFavicon.getValue();
} }
async clear(userId?: string): Promise<void> {
if (userId == null || userId == (await this.stateService.getUserId())) {
this._settings.next({});
}
await this.stateService.setSettings(null, { userId: userId });
}
} }

View File

@ -29,6 +29,7 @@ import { PolicyMigrator } from "./migrations/30-move-policy-state-to-state-provi
import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-menu-to-autofill-settings-state-provider"; import { EnableContextMenuMigrator } from "./migrations/31-move-enable-context-menu-to-autofill-settings-state-provider";
import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language"; import { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language";
import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers"; import { AppIdMigrator } from "./migrations/33-move-app-id-to-state-providers";
import { DomainSettingsMigrator } from "./migrations/34-move-domain-settings-to-state-providers";
import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked"; import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been-unlocked";
import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys";
import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key";
@ -38,7 +39,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting
import { MinVersionMigrator } from "./migrations/min-version"; import { MinVersionMigrator } from "./migrations/min-version";
export const MIN_VERSION = 2; export const MIN_VERSION = 2;
export const CURRENT_VERSION = 33; export const CURRENT_VERSION = 34;
export type MinVersion = typeof MIN_VERSION; export type MinVersion = typeof MIN_VERSION;
export function createMigrationBuilder() { export function createMigrationBuilder() {
@ -74,7 +75,8 @@ export function createMigrationBuilder() {
.with(PolicyMigrator, 29, 30) .with(PolicyMigrator, 29, 30)
.with(EnableContextMenuMigrator, 30, 31) .with(EnableContextMenuMigrator, 30, 31)
.with(PreferredLanguageMigrator, 31, 32) .with(PreferredLanguageMigrator, 31, 32)
.with(AppIdMigrator, 32, CURRENT_VERSION); .with(AppIdMigrator, 32, 33)
.with(DomainSettingsMigrator, 33, CURRENT_VERSION);
} }
export async function currentVersion( export async function currentVersion(

View File

@ -0,0 +1,255 @@
import { any, MockProxy } from "jest-mock-extended";
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
import { mockMigrationHelper } from "../migration-helper.spec";
import { DomainSettingsMigrator } from "./34-move-domain-settings-to-state-providers";
const mockNeverDomains = { "bitwarden.test": null, locahost: null, "www.example.com": null } as {
[key: string]: null;
};
function exampleJSON() {
return {
global: {
otherStuff: "otherStuff1",
neverDomains: mockNeverDomains,
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
settings: {
defaultUriMatch: 3,
settings: {
equivalentDomains: [] as string[][],
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
settings: {
settings: {
equivalentDomains: [["apple.com", "icloud.com"]],
},
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
"user-3": {
settings: {
defaultUriMatch: 1,
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
},
"user-4": {
settings: {
otherStuff: "otherStuff8",
},
otherStuff: "otherStuff9",
},
};
}
function rollbackJSON() {
return {
global_domainSettings_neverDomains: mockNeverDomains,
"user_user-1_domainSettings_defaultUriMatchStrategy": 3,
"user_user-1_domainSettings_equivalentDomains": [] as string[][],
"user_user-2_domainSettings_equivalentDomains": [["apple.com", "icloud.com"]],
"user_user-3_domainSettings_defaultUriMatchStrategy": 1,
global: {
otherStuff: "otherStuff1",
},
authenticatedAccounts: ["user-1", "user-2", "user-3"],
"user-1": {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
},
"user-2": {
settings: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
},
"user-3": {
settings: {
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
},
"user-4": {
settings: {
otherStuff: "otherStuff8",
},
otherStuff: "otherStuff9",
},
};
}
const domainSettingsStateDefinition: {
stateDefinition: StateDefinitionLike;
} = {
stateDefinition: {
name: "domainSettings",
},
};
describe("DomainSettingsMigrator", () => {
let helper: MockProxy<MigrationHelper>;
let sut: DomainSettingsMigrator;
describe("migrate", () => {
beforeEach(() => {
helper = mockMigrationHelper(exampleJSON(), 33);
sut = new DomainSettingsMigrator(33, 34);
});
it("should remove global neverDomains and defaultUriMatch and equivalentDomains settings from all accounts", async () => {
await sut.migrate(helper);
expect(helper.set).toHaveBeenCalledTimes(4);
expect(helper.set).toHaveBeenCalledWith("global", {
otherStuff: "otherStuff1",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-2", {
settings: {
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
});
expect(helper.set).toHaveBeenCalledWith("user-3", {
settings: {
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
});
});
it("should set global neverDomains and defaultUriMatchStrategy and equivalentDomains setting values for each account", async () => {
await sut.migrate(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...domainSettingsStateDefinition, key: "neverDomains" },
mockNeverDomains,
);
expect(helper.setToUser).toHaveBeenCalledTimes(4);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
3,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
[],
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-2",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
[["apple.com", "icloud.com"]],
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-3",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
1,
);
});
});
describe("rollback", () => {
beforeEach(() => {
helper = mockMigrationHelper(rollbackJSON(), 34);
sut = new DomainSettingsMigrator(33, 34);
});
it("should null out new values globally and for each account", async () => {
await sut.rollback(helper);
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
expect(helper.setToGlobal).toHaveBeenCalledWith(
{ ...domainSettingsStateDefinition, key: "neverDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledTimes(4);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-1",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-2",
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
null,
);
expect(helper.setToUser).toHaveBeenCalledWith(
"user-3",
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
null,
);
});
it("should add explicit value back to accounts", async () => {
await sut.rollback(helper);
expect(helper.set).toHaveBeenCalledTimes(4);
expect(helper.set).toHaveBeenCalledWith("global", {
neverDomains: mockNeverDomains,
otherStuff: "otherStuff1",
});
expect(helper.set).toHaveBeenCalledWith("user-1", {
settings: {
defaultUriMatch: 3,
settings: {
equivalentDomains: [] as string[][],
},
otherStuff: "otherStuff2",
},
otherStuff: "otherStuff3",
});
expect(helper.set).toHaveBeenCalledWith("user-2", {
settings: {
settings: {
equivalentDomains: [["apple.com", "icloud.com"]],
},
otherStuff: "otherStuff4",
},
otherStuff: "otherStuff5",
});
expect(helper.set).toHaveBeenCalledWith("user-3", {
settings: {
defaultUriMatch: 1,
otherStuff: "otherStuff6",
},
otherStuff: "otherStuff7",
});
});
it("should not try to restore values to missing accounts", async () => {
await sut.rollback(helper);
expect(helper.set).not.toHaveBeenCalledWith("user-4", any());
});
});
});

View File

@ -0,0 +1,167 @@
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
import { Migrator } from "../migrator";
const UriMatchStrategy = {
Domain: 0,
Host: 1,
StartsWith: 2,
Exact: 3,
RegularExpression: 4,
Never: 5,
} as const;
type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
type ExpectedAccountState = {
settings?: {
defaultUriMatch?: UriMatchStrategySetting;
settings?: {
equivalentDomains?: string[][];
};
};
};
type ExpectedGlobalState = {
neverDomains?: { [key: string]: null };
};
const defaultUriMatchStrategyDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "defaultUriMatchStrategy",
};
const equivalentDomainsDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "equivalentDomains",
};
const neverDomainsDefinition: KeyDefinitionLike = {
stateDefinition: {
name: "domainSettings",
},
key: "neverDomains",
};
export class DomainSettingsMigrator extends Migrator<33, 34> {
async migrate(helper: MigrationHelper): Promise<void> {
let updateAccount = false;
// global state ("neverDomains")
const globalState = await helper.get<ExpectedGlobalState>("global");
if (globalState?.neverDomains != null) {
await helper.setToGlobal(neverDomainsDefinition, globalState.neverDomains);
// delete `neverDomains` from state global
delete globalState.neverDomains;
await helper.set<ExpectedGlobalState>("global", globalState);
}
// account state ("defaultUriMatch" and "settings.equivalentDomains")
const accounts = await helper.getAccounts<ExpectedAccountState>();
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
// migrate account state
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
const accountSettings = account?.settings;
if (accountSettings?.defaultUriMatch != undefined) {
await helper.setToUser(
userId,
defaultUriMatchStrategyDefinition,
accountSettings.defaultUriMatch,
);
delete account.settings.defaultUriMatch;
updateAccount = true;
}
if (accountSettings?.settings?.equivalentDomains != undefined) {
await helper.setToUser(
userId,
equivalentDomainsDefinition,
accountSettings.settings.equivalentDomains,
);
delete account.settings.settings.equivalentDomains;
delete account.settings.settings;
updateAccount = true;
}
if (updateAccount) {
// update the state account settings with the migrated values deleted
await helper.set(userId, account);
}
}
}
async rollback(helper: MigrationHelper): Promise<void> {
let updateAccount = false;
// global state ("neverDomains")
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
const neverDomains: { [key: string]: null } =
await helper.getFromGlobal(neverDomainsDefinition);
if (neverDomains != null) {
await helper.set<ExpectedGlobalState>("global", {
...globalState,
neverDomains: neverDomains,
});
// remove the global state provider framework key for `neverDomains`
await helper.setToGlobal(neverDomainsDefinition, null);
}
// account state ("defaultUriMatchStrategy" and "equivalentDomains")
const accounts = await helper.getAccounts<ExpectedAccountState>();
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
// rollback account state
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
let settings = account?.settings || {};
const defaultUriMatchStrategy: UriMatchStrategySetting = await helper.getFromUser(
userId,
defaultUriMatchStrategyDefinition,
);
const equivalentDomains: string[][] = await helper.getFromUser(
userId,
equivalentDomainsDefinition,
);
// update new settings and remove the account state provider framework keys for the rolled back values
if (defaultUriMatchStrategy != null) {
settings = { ...settings, defaultUriMatch: defaultUriMatchStrategy };
await helper.setToUser(userId, defaultUriMatchStrategyDefinition, null);
updateAccount = true;
}
if (equivalentDomains != null) {
settings = { ...settings, settings: { equivalentDomains } };
await helper.setToUser(userId, equivalentDomainsDefinition, null);
updateAccount = true;
}
// commit updated settings to state
if (updateAccount) {
await helper.set(userId, {
...account,
settings,
});
}
}
}
}

View File

@ -1,5 +1,5 @@
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../enums";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data"; import { CipherData } from "../models/data/cipher.data";
import { Cipher } from "../models/domain/cipher"; import { Cipher } from "../models/domain/cipher";
@ -24,7 +24,7 @@ export abstract class CipherService {
getAllDecryptedForUrl: ( getAllDecryptedForUrl: (
url: string, url: string,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchType, defaultMatch?: UriMatchStrategySetting,
) => Promise<CipherView[]>; ) => Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
/** /**

View File

@ -3,4 +3,3 @@ export * from "./cipher-type";
export * from "./field-type.enum"; export * from "./field-type.enum";
export * from "./linked-id-type.enum"; export * from "./linked-id-type.enum";
export * from "./secure-note-type.enum"; export * from "./secure-note-type.enum";
export * from "./uri-match-type.enum";

View File

@ -1,8 +0,0 @@
export enum UriMatchType {
Domain = 0,
Host = 1,
StartsWith = 2,
Exact = 3,
RegularExpression = 4,
Never = 5,
}

View File

@ -1,10 +1,10 @@
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { BaseResponse } from "../../../models/response/base.response"; import { BaseResponse } from "../../../models/response/base.response";
import { UriMatchType } from "../../enums";
export class LoginUriApi extends BaseResponse { export class LoginUriApi extends BaseResponse {
uri: string; uri: string;
uriChecksum: string; uriChecksum: string;
match: UriMatchType = null; match: UriMatchStrategySetting = null;
constructor(data: any = null) { constructor(data: any = null) {
super(data); super(data);

View File

@ -1,10 +1,10 @@
import { UriMatchType } from "../../enums"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { LoginUriApi } from "../api/login-uri.api"; import { LoginUriApi } from "../api/login-uri.api";
export class LoginUriData { export class LoginUriData {
uri: string; uri: string;
uriChecksum: string; uriChecksum: string;
match: UriMatchType = null; match: UriMatchStrategySetting = null;
constructor(data?: LoginUriApi) { constructor(data?: LoginUriApi) {
if (data == null) { if (data == null) {

View File

@ -2,13 +2,14 @@ import { mock } from "jest-mock-extended";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { ContainerService } from "../../../platform/services/container.service"; import { ContainerService } from "../../../platform/services/container.service";
import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key";
import { CipherService } from "../../abstractions/cipher.service"; import { CipherService } from "../../abstractions/cipher.service";
import { FieldType, SecureNoteType, UriMatchType } from "../../enums"; import { FieldType, SecureNoteType } from "../../enums";
import { CipherRepromptType } from "../../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../enums/cipher-reprompt-type";
import { CipherType } from "../../enums/cipher-type"; import { CipherType } from "../../enums/cipher-type";
import { CipherData } from "../../models/data/cipher.data"; import { CipherData } from "../../models/data/cipher.data";
@ -76,7 +77,11 @@ describe("Cipher DTO", () => {
key: "EncryptedString", key: "EncryptedString",
login: { login: {
uris: [ uris: [
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain }, {
uri: "EncryptedString",
uriChecksum: "EncryptedString",
match: UriMatchStrategy.Domain,
},
], ],
username: "EncryptedString", username: "EncryptedString",
password: "EncryptedString", password: "EncryptedString",

View File

@ -2,9 +2,9 @@ import { MockProxy, mock } from "jest-mock-extended";
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchStrategy } from "../../../models/domain/domain-service";
import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data"; import { LoginUriData } from "../data/login-uri.data";
import { LoginUri } from "./login-uri"; import { LoginUri } from "./login-uri";
@ -16,7 +16,7 @@ describe("LoginUri", () => {
data = { data = {
uri: "encUri", uri: "encUri",
uriChecksum: "encUriChecksum", uriChecksum: "encUriChecksum",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
}; };
}); });
@ -48,7 +48,7 @@ describe("LoginUri", () => {
it("Decrypt", async () => { it("Decrypt", async () => {
const loginUri = new LoginUri(); const loginUri = new LoginUri();
loginUri.match = UriMatchType.Exact; loginUri.match = UriMatchStrategy.Exact;
loginUri.uri = mockEnc("uri"); loginUri.uri = mockEnc("uri");
const view = await loginUri.decrypt(null); const view = await loginUri.decrypt(null);
@ -103,13 +103,13 @@ describe("LoginUri", () => {
const actual = LoginUri.fromJSON({ const actual = LoginUri.fromJSON({
uri: "myUri", uri: "myUri",
uriChecksum: "myUriChecksum", uriChecksum: "myUriChecksum",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
} as Jsonify<LoginUri>); } as Jsonify<LoginUri>);
expect(actual).toEqual({ expect(actual).toEqual({
uri: "myUri_fromJSON", uri: "myUri_fromJSON",
uriChecksum: "myUriChecksum_fromJSON", uriChecksum: "myUriChecksum_fromJSON",
match: UriMatchType.Domain, match: UriMatchStrategy.Domain,
}); });
expect(actual).toBeInstanceOf(LoginUri); expect(actual).toBeInstanceOf(LoginUri);
}); });

View File

@ -1,17 +1,17 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import Domain from "../../../platform/models/domain/domain-base"; import Domain from "../../../platform/models/domain/domain-base";
import { EncString } from "../../../platform/models/domain/enc-string"; import { EncString } from "../../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key";
import { UriMatchType } from "../../enums";
import { LoginUriData } from "../data/login-uri.data"; import { LoginUriData } from "../data/login-uri.data";
import { LoginUriView } from "../view/login-uri.view"; import { LoginUriView } from "../view/login-uri.view";
export class LoginUri extends Domain { export class LoginUri extends Domain {
uri: EncString; uri: EncString;
uriChecksum: EncString | undefined; uriChecksum: EncString | undefined;
match: UriMatchType; match: UriMatchStrategySetting;
constructor(obj?: LoginUriData) { constructor(obj?: LoginUriData) {
super(); super();

View File

@ -1,8 +1,8 @@
import { MockProxy, mock } from "jest-mock-extended"; import { MockProxy, mock } from "jest-mock-extended";
import { mockEnc, mockFromJson } from "../../../../spec"; import { mockEnc, mockFromJson } from "../../../../spec";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string";
import { UriMatchType } from "../../enums";
import { LoginData } from "../../models/data/login.data"; import { LoginData } from "../../models/data/login.data";
import { Login } from "../../models/domain/login"; import { Login } from "../../models/domain/login";
import { LoginUri } from "../../models/domain/login-uri"; import { LoginUri } from "../../models/domain/login-uri";
@ -30,7 +30,7 @@ describe("Login DTO", () => {
it("Convert from full LoginData", () => { it("Convert from full LoginData", () => {
const fido2CredentialData = initializeFido2Credential(new Fido2CredentialData()); const fido2CredentialData = initializeFido2Credential(new Fido2CredentialData());
const data: LoginData = { const data: LoginData = {
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }], uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
username: "username", username: "username",
password: "password", password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z", passwordRevisionDate: "2022-01-31T12:00:00.000Z",
@ -82,7 +82,7 @@ describe("Login DTO", () => {
totp: "encrypted totp", totp: "encrypted totp",
uris: [ uris: [
{ {
match: null as UriMatchType, match: null as UriMatchStrategySetting,
_uri: "decrypted uri", _uri: "decrypted uri",
_domain: null as string, _domain: null as string,
_hostname: null as string, _hostname: null as string,
@ -123,7 +123,7 @@ describe("Login DTO", () => {
it("Converts from LoginData and back", () => { it("Converts from LoginData and back", () => {
const data: LoginData = { const data: LoginData = {
uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }], uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }],
username: "username", username: "username",
password: "password", password: "password",
passwordRevisionDate: "2022-01-31T12:00:00.000Z", passwordRevisionDate: "2022-01-31T12:00:00.000Z",

View File

@ -1,26 +1,26 @@
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUriView } from "./login-uri.view"; import { LoginUriView } from "./login-uri.view";
const testData = [ const testData = [
{ {
match: UriMatchType.Host, match: UriMatchStrategy.Host,
uri: "http://example.com/login", uri: "http://example.com/login",
expected: "http://example.com/login", expected: "http://example.com/login",
}, },
{ {
match: UriMatchType.Host, match: UriMatchStrategy.Host,
uri: "bitwarden.com", uri: "bitwarden.com",
expected: "http://bitwarden.com", expected: "http://bitwarden.com",
}, },
{ {
match: UriMatchType.Host, match: UriMatchStrategy.Host,
uri: "bitwarden.de", uri: "bitwarden.de",
expected: "http://bitwarden.de", expected: "http://bitwarden.de",
}, },
{ {
match: UriMatchType.Host, match: UriMatchStrategy.Host,
uri: "bitwarden.br", uri: "bitwarden.br",
expected: "http://bitwarden.br", expected: "http://bitwarden.br",
}, },
@ -41,7 +41,7 @@ const exampleUris = {
describe("LoginUriView", () => { describe("LoginUriView", () => {
it("isWebsite() given an invalid domain should return false", async () => { it("isWebsite() given an invalid domain should return false", async () => {
const uri = new LoginUriView(); const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.Host, uri: "bit!:_&ward.com" }); Object.assign(uri, { match: UriMatchStrategy.Host, uri: "bit!:_&ward.com" });
expect(uri.isWebsite).toBe(false); expect(uri.isWebsite).toBe(false);
}); });
@ -67,32 +67,32 @@ describe("LoginUriView", () => {
it(`canLaunch should return false when MatchDetection is set to Regex`, async () => { it(`canLaunch should return false when MatchDetection is set to Regex`, async () => {
const uri = new LoginUriView(); const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.RegularExpression, uri: "bitwarden.com" }); Object.assign(uri, { match: UriMatchStrategy.RegularExpression, uri: "bitwarden.com" });
expect(uri.canLaunch).toBe(false); expect(uri.canLaunch).toBe(false);
}); });
it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => { it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => {
const uri = new LoginUriView(); const uri = new LoginUriView();
Object.assign(uri, { match: UriMatchType.Host, uri: "someprotocol://bitwarden.com" }); Object.assign(uri, { match: UriMatchStrategy.Host, uri: "someprotocol://bitwarden.com" });
expect(uri.canLaunch).toBe(false); expect(uri.canLaunch).toBe(false);
}); });
describe("uri matching", () => { describe("uri matching", () => {
describe("using domain matching", () => { describe("using domain matching", () => {
it("matches the same domain", () => { it("matches the same domain", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.subdomain, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.subdomain, exampleUris.noEquivalentDomains());
expect(actual).toBe(true); expect(actual).toBe(true);
}); });
it("matches equivalent domains", () => { it("matches equivalent domains", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.differentDomain, exampleUris.equivalentDomains()); const actual = uri.matchesUri(exampleUris.differentDomain, exampleUris.equivalentDomains());
expect(actual).toBe(true); expect(actual).toBe(true);
}); });
it("does not match a different domain", () => { it("does not match a different domain", () => {
const uri = uriFactory(UriMatchType.Domain, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Domain, exampleUris.standard);
const actual = uri.matchesUri( const actual = uri.matchesUri(
exampleUris.differentDomain, exampleUris.differentDomain,
exampleUris.noEquivalentDomains(), exampleUris.noEquivalentDomains(),
@ -103,7 +103,7 @@ describe("LoginUriView", () => {
// Actual integration test with the real blacklist, not ideal // Actual integration test with the real blacklist, not ideal
it("does not match domains that are blacklisted", () => { it("does not match domains that are blacklisted", () => {
const googleEquivalentDomains = new Set(["google.com", "script.google.com"]); const googleEquivalentDomains = new Set(["google.com", "script.google.com"]);
const uri = uriFactory(UriMatchType.Domain, "google.com"); const uri = uriFactory(UriMatchStrategy.Domain, "google.com");
const actual = uri.matchesUri("script.google.com", googleEquivalentDomains); const actual = uri.matchesUri("script.google.com", googleEquivalentDomains);
@ -113,13 +113,13 @@ describe("LoginUriView", () => {
describe("using host matching", () => { describe("using host matching", () => {
it("matches the same host", () => { it("matches the same host", () => {
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.standard)); const uri = uriFactory(UriMatchStrategy.Host, Utils.getHost(exampleUris.standard));
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(true); expect(actual).toBe(true);
}); });
it("does not match a different host", () => { it("does not match a different host", () => {
const uri = uriFactory(UriMatchType.Host, Utils.getHost(exampleUris.differentDomain)); const uri = uriFactory(UriMatchStrategy.Host, Utils.getHost(exampleUris.differentDomain));
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(false); expect(actual).toBe(false);
}); });
@ -127,13 +127,13 @@ describe("LoginUriView", () => {
describe("using exact matching", () => { describe("using exact matching", () => {
it("matches if both uris are the same", () => { it("matches if both uris are the same", () => {
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Exact, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(true); expect(actual).toBe(true);
}); });
it("does not match if the uris are different", () => { it("does not match if the uris are different", () => {
const uri = uriFactory(UriMatchType.Exact, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Exact, exampleUris.standard);
const actual = uri.matchesUri( const actual = uri.matchesUri(
exampleUris.standard + "#", exampleUris.standard + "#",
exampleUris.noEquivalentDomains(), exampleUris.noEquivalentDomains(),
@ -144,7 +144,7 @@ describe("LoginUriView", () => {
describe("using startsWith matching", () => { describe("using startsWith matching", () => {
it("matches if the target URI starts with the saved URI", () => { it("matches if the target URI starts with the saved URI", () => {
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.StartsWith, exampleUris.standard);
const actual = uri.matchesUri( const actual = uri.matchesUri(
exampleUris.standard + "#bookmark", exampleUris.standard + "#bookmark",
exampleUris.noEquivalentDomains(), exampleUris.noEquivalentDomains(),
@ -153,7 +153,7 @@ describe("LoginUriView", () => {
}); });
it("does not match if the start of the uri is not the same", () => { it("does not match if the start of the uri is not the same", () => {
const uri = uriFactory(UriMatchType.StartsWith, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.StartsWith, exampleUris.standard);
const actual = uri.matchesUri( const actual = uri.matchesUri(
exampleUris.standard.slice(1), exampleUris.standard.slice(1),
exampleUris.noEquivalentDomains(), exampleUris.noEquivalentDomains(),
@ -164,13 +164,13 @@ describe("LoginUriView", () => {
describe("using regular expression matching", () => { describe("using regular expression matching", () => {
it("matches if the regular expression matches", () => { it("matches if the regular expression matches", () => {
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.RegularExpression, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
expect(actual).toBe(false); expect(actual).toBe(false);
}); });
it("does not match if the regular expression does not match", () => { it("does not match if the regular expression does not match", () => {
const uri = uriFactory(UriMatchType.RegularExpression, exampleUris.standardNotMatching); const uri = uriFactory(UriMatchStrategy.RegularExpression, exampleUris.standardNotMatching);
const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standardRegex, exampleUris.noEquivalentDomains());
expect(actual).toBe(false); expect(actual).toBe(false);
}); });
@ -178,7 +178,7 @@ describe("LoginUriView", () => {
describe("using never matching", () => { describe("using never matching", () => {
it("does not match even if uris are identical", () => { it("does not match even if uris are identical", () => {
const uri = uriFactory(UriMatchType.Never, exampleUris.standard); const uri = uriFactory(UriMatchStrategy.Never, exampleUris.standard);
const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains()); const actual = uri.matchesUri(exampleUris.standard, exampleUris.noEquivalentDomains());
expect(actual).toBe(false); expect(actual).toBe(false);
}); });
@ -186,7 +186,7 @@ describe("LoginUriView", () => {
}); });
}); });
function uriFactory(match: UriMatchType, uri: string) { function uriFactory(match: UriMatchStrategySetting, uri: string) {
const loginUri = new LoginUriView(); const loginUri = new LoginUriView();
loginUri.match = match; loginUri.match = match;
loginUri.uri = uri; loginUri.uri = uri;

View File

@ -1,13 +1,13 @@
import { Jsonify } from "type-fest"; import { Jsonify } from "type-fest";
import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { View } from "../../../models/view/view"; import { View } from "../../../models/view/view";
import { SafeUrls } from "../../../platform/misc/safe-urls"; import { SafeUrls } from "../../../platform/misc/safe-urls";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { UriMatchType } from "../../enums";
import { LoginUri } from "../domain/login-uri"; import { LoginUri } from "../domain/login-uri";
export class LoginUriView implements View { export class LoginUriView implements View {
match: UriMatchType = null; match: UriMatchStrategySetting = null;
private _uri: string = null; private _uri: string = null;
private _domain: string = null; private _domain: string = null;
@ -44,7 +44,7 @@ export class LoginUriView implements View {
} }
get hostname(): string { get hostname(): string {
if (this.match === UriMatchType.RegularExpression) { if (this.match === UriMatchStrategy.RegularExpression) {
return null; return null;
} }
if (this._hostname == null && this.uri != null) { if (this._hostname == null && this.uri != null) {
@ -58,7 +58,7 @@ export class LoginUriView implements View {
} }
get host(): string { get host(): string {
if (this.match === UriMatchType.RegularExpression) { if (this.match === UriMatchStrategy.RegularExpression) {
return null; return null;
} }
if (this._host == null && this.uri != null) { if (this._host == null && this.uri != null) {
@ -92,7 +92,7 @@ export class LoginUriView implements View {
if (this._canLaunch != null) { if (this._canLaunch != null) {
return this._canLaunch; return this._canLaunch;
} }
if (this.uri != null && this.match !== UriMatchType.RegularExpression) { if (this.uri != null && this.match !== UriMatchStrategy.RegularExpression) {
this._canLaunch = SafeUrls.canLaunch(this.launchUri); this._canLaunch = SafeUrls.canLaunch(this.launchUri);
} else { } else {
this._canLaunch = false; this._canLaunch = false;
@ -113,30 +113,30 @@ export class LoginUriView implements View {
matchesUri( matchesUri(
targetUri: string, targetUri: string,
equivalentDomains: Set<string>, equivalentDomains: Set<string>,
defaultUriMatch: UriMatchType = null, defaultUriMatch: UriMatchStrategySetting = null,
): boolean { ): boolean {
if (!this.uri || !targetUri) { if (!this.uri || !targetUri) {
return false; return false;
} }
let matchType = this.match ?? defaultUriMatch; let matchType = this.match ?? defaultUriMatch;
matchType ??= UriMatchType.Domain; matchType ??= UriMatchStrategy.Domain;
const targetDomain = Utils.getDomain(targetUri); const targetDomain = Utils.getDomain(targetUri);
const matchDomains = equivalentDomains.add(targetDomain); const matchDomains = equivalentDomains.add(targetDomain);
switch (matchType) { switch (matchType) {
case UriMatchType.Domain: case UriMatchStrategy.Domain:
return this.matchesDomain(targetUri, matchDomains); return this.matchesDomain(targetUri, matchDomains);
case UriMatchType.Host: { case UriMatchStrategy.Host: {
const urlHost = Utils.getHost(targetUri); const urlHost = Utils.getHost(targetUri);
return urlHost != null && urlHost === Utils.getHost(this.uri); return urlHost != null && urlHost === Utils.getHost(this.uri);
} }
case UriMatchType.Exact: case UriMatchStrategy.Exact:
return targetUri === this.uri; return targetUri === this.uri;
case UriMatchType.StartsWith: case UriMatchStrategy.StartsWith:
return targetUri.startsWith(this.uri); return targetUri.startsWith(this.uri);
case UriMatchType.RegularExpression: case UriMatchStrategy.RegularExpression:
try { try {
const regex = new RegExp(this.uri, "i"); const regex = new RegExp(this.uri, "i");
return regex.test(targetUri); return regex.test(targetUri);
@ -144,7 +144,7 @@ export class LoginUriView implements View {
// Invalid regex // Invalid regex
return false; return false;
} }
case UriMatchType.Never: case UriMatchStrategy.Never:
return false; return false;
default: default:
break; break;

View File

@ -1,6 +1,7 @@
import { UriMatchStrategySetting } from "../../../models/domain/domain-service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
import { DeepJsonify } from "../../../types/deep-jsonify"; import { DeepJsonify } from "../../../types/deep-jsonify";
import { LoginLinkedId as LinkedId, UriMatchType } from "../../enums"; import { LoginLinkedId as LinkedId } from "../../enums";
import { linkedFieldOption } from "../../linked-field-option.decorator"; import { linkedFieldOption } from "../../linked-field-option.decorator";
import { Login } from "../domain/login"; import { Login } from "../domain/login";
@ -71,7 +72,7 @@ export class LoginView extends ItemView {
matchesUri( matchesUri(
targetUri: string, targetUri: string,
equivalentDomains: Set<string>, equivalentDomains: Set<string>,
defaultUriMatch: UriMatchType = null, defaultUriMatch: UriMatchStrategySetting = null,
): boolean { ): boolean {
if (this.uris == null) { if (this.uris == null) {
return false; return false;

View File

@ -4,8 +4,9 @@ import { of } from "rxjs";
import { makeStaticByteArray } from "../../../spec/utils"; import { makeStaticByteArray } from "../../../spec/utils";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service"; import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service"; import { AutofillSettingsService } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { UriMatchStrategy } from "../../models/domain/domain-service";
import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "../../platform/abstractions/config/config.service.abstraction";
import { CryptoService } from "../../platform/abstractions/crypto.service"; import { CryptoService } from "../../platform/abstractions/crypto.service";
import { EncryptService } from "../../platform/abstractions/encrypt.service"; import { EncryptService } from "../../platform/abstractions/encrypt.service";
@ -17,7 +18,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
import { ContainerService } from "../../platform/services/container.service"; import { ContainerService } from "../../platform/services/container.service";
import { CipherKey, OrgKey } from "../../types/key"; import { CipherKey, OrgKey } from "../../types/key";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { UriMatchType, FieldType } from "../enums"; import { FieldType } from "../enums";
import { CipherRepromptType } from "../enums/cipher-reprompt-type"; import { CipherRepromptType } from "../enums/cipher-reprompt-type";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data"; import { CipherData } from "../models/data/cipher.data";
@ -53,7 +54,9 @@ const cipherData: CipherData = {
key: "EncKey", key: "EncKey",
reprompt: CipherRepromptType.None, reprompt: CipherRepromptType.None,
login: { login: {
uris: [{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain }], uris: [
{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain },
],
username: "EncryptedString", username: "EncryptedString",
password: "EncryptedString", password: "EncryptedString",
passwordRevisionDate: "2022-01-31T12:00:00.000Z", passwordRevisionDate: "2022-01-31T12:00:00.000Z",
@ -99,7 +102,7 @@ describe("Cipher Service", () => {
const cryptoService = mock<CryptoService>(); const cryptoService = mock<CryptoService>();
const stateService = mock<StateService>(); const stateService = mock<StateService>();
const autofillSettingsService = mock<AutofillSettingsService>(); const autofillSettingsService = mock<AutofillSettingsService>();
const settingsService = mock<SettingsService>(); const domainSettingsService = mock<DomainSettingsService>();
const apiService = mock<ApiService>(); const apiService = mock<ApiService>();
const cipherFileUploadService = mock<CipherFileUploadService>(); const cipherFileUploadService = mock<CipherFileUploadService>();
const i18nService = mock<I18nService>(); const i18nService = mock<I18nService>();
@ -118,7 +121,7 @@ describe("Cipher Service", () => {
cipherService = new CipherService( cipherService = new CipherService(
cryptoService, cryptoService,
settingsService, domainSettingsService,
apiService, apiService,
i18nService, i18nService,
searchService, searchService,
@ -277,7 +280,7 @@ describe("Cipher Service", () => {
it("should add a uri hash to login uris", async () => { it("should add a uri hash to login uris", async () => {
encryptService.hash.mockImplementation((value) => Promise.resolve(`${value} hash`)); encryptService.hash.mockImplementation((value) => Promise.resolve(`${value} hash`));
cipherView.login.uris = [ cipherView.login.uris = [
{ uri: "uri", match: UriMatchType.RegularExpression } as LoginUriView, { uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView,
]; ];
const domain = await cipherService.encrypt(cipherView); const domain = await cipherService.encrypt(cipherView);
@ -286,7 +289,7 @@ describe("Cipher Service", () => {
{ {
uri: new EncString("uri has been encrypted"), uri: new EncString("uri has been encrypted"),
uriChecksum: new EncString("uri hash has been encrypted"), uriChecksum: new EncString("uri hash has been encrypted"),
match: UriMatchType.RegularExpression, match: UriMatchStrategy.RegularExpression,
}, },
]); ]);
}); });

View File

@ -3,8 +3,9 @@ import { SemVer } from "semver";
import { ApiService } from "../../abstractions/api.service"; import { ApiService } from "../../abstractions/api.service";
import { SearchService } from "../../abstractions/search.service"; import { SearchService } from "../../abstractions/search.service";
import { SettingsService } from "../../abstractions/settings.service";
import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service"; import { AutofillSettingsServiceAbstraction } from "../../autofill/services/autofill-settings.service";
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { ErrorResponse } from "../../models/response/error.response"; import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response"; import { ListResponse } from "../../models/response/list.response";
import { View } from "../../models/view/view"; import { View } from "../../models/view/view";
@ -23,7 +24,7 @@ import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypt
import { UserKey, OrgKey } from "../../types/key"; import { UserKey, OrgKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service"; import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
import { FieldType, UriMatchType } from "../enums"; import { FieldType } from "../enums";
import { CipherType } from "../enums/cipher-type"; import { CipherType } from "../enums/cipher-type";
import { CipherData } from "../models/data/cipher.data"; import { CipherData } from "../models/data/cipher.data";
import { Attachment } from "../models/domain/attachment"; import { Attachment } from "../models/domain/attachment";
@ -61,7 +62,7 @@ export class CipherService implements CipherServiceAbstraction {
constructor( constructor(
private cryptoService: CryptoService, private cryptoService: CryptoService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private apiService: ApiService, private apiService: ApiService,
private i18nService: I18nService, private i18nService: I18nService,
private searchService: SearchService, private searchService: SearchService,
@ -355,15 +356,17 @@ export class CipherService implements CipherServiceAbstraction {
async getAllDecryptedForUrl( async getAllDecryptedForUrl(
url: string, url: string,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch: UriMatchType = null, defaultMatch: UriMatchStrategySetting = null,
): Promise<CipherView[]> { ): Promise<CipherView[]> {
if (url == null && includeOtherTypes == null) { if (url == null && includeOtherTypes == null) {
return Promise.resolve([]); return Promise.resolve([]);
} }
const equivalentDomains = this.settingsService.getEquivalentDomains(url); const equivalentDomains = await firstValueFrom(
this.domainSettingsService.getUrlEquivalentDomains(url),
);
const ciphers = await this.getAllDecrypted(); const ciphers = await this.getAllDecrypted();
defaultMatch ??= await this.stateService.getDefaultUriMatch(); defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$);
return ciphers.filter((cipher) => { return ciphers.filter((cipher) => {
const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null; const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null;
@ -503,12 +506,12 @@ export class CipherService implements CipherServiceAbstraction {
return; return;
} }
let domains = await this.stateService.getNeverDomains(); let domains = await firstValueFrom(this.domainSettingsService.neverDomains$);
if (!domains) { if (!domains) {
domains = {}; domains = {};
} }
domains[domain] = null; domains[domain] = null;
await this.stateService.setNeverDomains(domains); await this.domainSettingsService.setNeverDomains(domains);
} }
async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<any> { async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<any> {

View File

@ -3,6 +3,7 @@ import { of } from "rxjs";
import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { StateService } from "../../../platform/abstractions/state.service"; import { StateService } from "../../../platform/abstractions/state.service";
import { Utils } from "../../../platform/misc/utils"; import { Utils } from "../../../platform/misc/utils";
@ -34,6 +35,7 @@ describe("FidoAuthenticatorService", () => {
let authService!: MockProxy<AuthService>; let authService!: MockProxy<AuthService>;
let stateService!: MockProxy<StateService>; let stateService!: MockProxy<StateService>;
let vaultSettingsService: MockProxy<VaultSettingsService>; let vaultSettingsService: MockProxy<VaultSettingsService>;
let domainSettingsService: MockProxy<DomainSettingsService>;
let client!: Fido2ClientService; let client!: Fido2ClientService;
let tab!: chrome.tabs.Tab; let tab!: chrome.tabs.Tab;
@ -43,6 +45,7 @@ describe("FidoAuthenticatorService", () => {
authService = mock<AuthService>(); authService = mock<AuthService>();
stateService = mock<StateService>(); stateService = mock<StateService>();
vaultSettingsService = mock<VaultSettingsService>(); vaultSettingsService = mock<VaultSettingsService>();
domainSettingsService = mock<DomainSettingsService>();
client = new Fido2ClientService( client = new Fido2ClientService(
authenticator, authenticator,
@ -50,9 +53,11 @@ describe("FidoAuthenticatorService", () => {
authService, authService,
stateService, stateService,
vaultSettingsService, vaultSettingsService,
domainSettingsService,
); );
configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any); configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any);
vaultSettingsService.enablePasskeys$ = of(true); vaultSettingsService.enablePasskeys$ = of(true);
domainSettingsService.neverDomains$ = of({});
authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked);
tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; tab = { id: 123, windowId: 456 } as chrome.tabs.Tab;
}); });
@ -130,7 +135,7 @@ describe("FidoAuthenticatorService", () => {
origin: "https://bitwarden.com", origin: "https://bitwarden.com",
rp: { id: "bitwarden.com", name: "Bitwarden" }, rp: { id: "bitwarden.com", name: "Bitwarden" },
}); });
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null }); domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.createCredential(params, tab); const result = async () => await client.createCredential(params, tab);
@ -376,7 +381,8 @@ describe("FidoAuthenticatorService", () => {
const params = createParams({ const params = createParams({
origin: "https://bitwarden.com", origin: "https://bitwarden.com",
}); });
stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null });
domainSettingsService.neverDomains$ = of({ "bitwarden.com": null });
const result = async () => await client.assertCredential(params, tab); const result = async () => await client.assertCredential(params, tab);

View File

@ -3,6 +3,7 @@ import { parse } from "tldts";
import { AuthService } from "../../../auth/abstractions/auth.service"; import { AuthService } from "../../../auth/abstractions/auth.service";
import { AuthenticationStatus } from "../../../auth/enums/authentication-status"; import { AuthenticationStatus } from "../../../auth/enums/authentication-status";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction"; import { ConfigServiceAbstraction } from "../../../platform/abstractions/config/config.service.abstraction";
import { LogService } from "../../../platform/abstractions/log.service"; import { LogService } from "../../../platform/abstractions/log.service";
import { StateService } from "../../../platform/abstractions/state.service"; import { StateService } from "../../../platform/abstractions/state.service";
@ -44,6 +45,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
private authService: AuthService, private authService: AuthService,
private stateService: StateService, private stateService: StateService,
private vaultSettingsService: VaultSettingsService, private vaultSettingsService: VaultSettingsService,
private domainSettingsService: DomainSettingsService,
private logService?: LogService, private logService?: LogService,
) {} ) {}
@ -52,7 +54,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction {
const isUserLoggedIn = const isUserLoggedIn =
(await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut; (await this.authService.getAuthStatus()) !== AuthenticationStatus.LoggedOut;
const neverDomains = await this.stateService.getNeverDomains(); const neverDomains = await firstValueFrom(this.domainSettingsService.neverDomains$);
const isExcludedDomain = neverDomains != null && hostname in neverDomains; const isExcludedDomain = neverDomains != null && hostname in neverDomains;
const serverConfig = await firstValueFrom(this.configService.serverConfig$); const serverConfig = await firstValueFrom(this.configService.serverConfig$);

View File

@ -1,5 +1,4 @@
import { ApiService } from "../../../abstractions/api.service"; import { ApiService } from "../../../abstractions/api.service";
import { SettingsService } from "../../../abstractions/settings.service";
import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction";
import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction";
import { ProviderService } from "../../../admin-console/abstractions/provider.service"; import { ProviderService } from "../../../admin-console/abstractions/provider.service";
@ -10,6 +9,7 @@ import { ProviderData } from "../../../admin-console/models/data/provider.data";
import { PolicyResponse } from "../../../admin-console/models/response/policy.response"; import { PolicyResponse } from "../../../admin-console/models/response/policy.response";
import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; import { KeyConnectorService } from "../../../auth/abstractions/key-connector.service";
import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason";
import { DomainSettingsService } from "../../../autofill/services/domain-settings.service";
import { DomainsResponse } from "../../../models/response/domains.response"; import { DomainsResponse } from "../../../models/response/domains.response";
import { import {
SyncCipherNotification, SyncCipherNotification,
@ -44,7 +44,7 @@ export class SyncService implements SyncServiceAbstraction {
constructor( constructor(
private apiService: ApiService, private apiService: ApiService,
private settingsService: SettingsService, private domainSettingsService: DomainSettingsService,
private folderService: InternalFolderService, private folderService: InternalFolderService,
private cipherService: CipherService, private cipherService: CipherService,
private cryptoService: CryptoService, private cryptoService: CryptoService,
@ -457,7 +457,7 @@ export class SyncService implements SyncServiceAbstraction {
}); });
} }
return this.settingsService.setEquivalentDomains(eqDomains); return this.domainSettingsService.setEquivalentDomains(eqDomains);
} }
private async syncPolicies(response: PolicyResponse[]) { private async syncPolicies(response: PolicyResponse[]) {