diff --git a/apps/browser/src/autofill/background/abstractions/notification.background.ts b/apps/browser/src/autofill/background/abstractions/notification.background.ts index 322ccd732d..ab704a1d37 100644 --- a/apps/browser/src/autofill/background/abstractions/notification.background.ts +++ b/apps/browser/src/autofill/background/abstractions/notification.background.ts @@ -1,3 +1,4 @@ +import { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { NotificationQueueMessageTypes } from "../../enums/notification-queue-message-type.enum"; @@ -111,6 +112,7 @@ type NotificationBackgroundExtensionMessageHandlers = { collectPageDetailsResponse: ({ message }: BackgroundMessageParam) => Promise; bgGetEnableChangedPasswordPrompt: () => Promise; bgGetEnableAddedLoginPrompt: () => Promise; + bgGetExcludedDomains: () => Promise; getWebVaultUrlForNotification: () => string; }; diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 7dde61b6fd..7d168780a1 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -4,6 +4,7 @@ import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { EnvironmentService } from "@bitwarden/common/platform/services/environment.service"; @@ -47,6 +48,7 @@ describe("NotificationBackground", () => { const folderService = mock(); const stateService = mock(); const userNotificationSettingsService = mock(); + const domainSettingsService = mock(); const environmentService = mock(); const logService = mock(); @@ -59,6 +61,7 @@ describe("NotificationBackground", () => { folderService, stateService, userNotificationSettingsService, + domainSettingsService, environmentService, logService, ); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index 44decc01a9..522109f0ed 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -5,7 +5,9 @@ import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { NeverDomains } from "@bitwarden/common/models/domain/domain-service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -60,6 +62,7 @@ export default class NotificationBackground { bgReopenUnlockPopout: ({ sender }) => this.openUnlockPopout(sender.tab), bgGetEnableChangedPasswordPrompt: () => this.getEnableChangedPasswordPrompt(), bgGetEnableAddedLoginPrompt: () => this.getEnableAddedLoginPrompt(), + bgGetExcludedDomains: () => this.getExcludedDomains(), getWebVaultUrlForNotification: () => this.getWebVaultUrl(), }; @@ -71,6 +74,7 @@ export default class NotificationBackground { private folderService: FolderService, private stateService: BrowserStateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, + private domainSettingsService: DomainSettingsService, private environmentService: EnvironmentService, private logService: LogService, ) {} @@ -99,6 +103,13 @@ export default class NotificationBackground { return await firstValueFrom(this.userNotificationSettingsService.enableAddedLoginPrompt$); } + /** + * Gets the neverDomains setting from the domain settings service. + */ + async getExcludedDomains(): Promise { + return await firstValueFrom(this.domainSettingsService.neverDomains$); + } + /** * 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. diff --git a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts index 94e2356821..bbbca2f16a 100644 --- a/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts +++ b/apps/browser/src/autofill/background/service_factories/autofill-service.factory.ts @@ -6,10 +6,6 @@ import { EventCollectionServiceInitOptions, eventCollectionServiceFactory, } from "../../../background/service-factories/event-collection-service.factory"; -import { - settingsServiceFactory, - SettingsServiceInitOptions, -} from "../../../background/service-factories/settings-service.factory"; import { CachedServices, factory, @@ -38,6 +34,10 @@ import { AutofillSettingsServiceInitOptions, autofillSettingsServiceFactory, } from "./autofill-settings-service.factory"; +import { + DomainSettingsServiceInitOptions, + domainSettingsServiceFactory, +} from "./domain-settings-service.factory"; type AutoFillServiceOptions = FactoryOptions; @@ -48,8 +48,8 @@ export type AutoFillServiceInitOptions = AutoFillServiceOptions & TotpServiceInitOptions & EventCollectionServiceInitOptions & LogServiceInitOptions & - SettingsServiceInitOptions & - UserVerificationServiceInitOptions; + UserVerificationServiceInitOptions & + DomainSettingsServiceInitOptions; export function autofillServiceFactory( cache: { autofillService?: AbstractAutoFillService } & CachedServices, @@ -67,7 +67,7 @@ export function autofillServiceFactory( await totpServiceFactory(cache, opts), await eventCollectionServiceFactory(cache, opts), await logServiceFactory(cache, opts), - await settingsServiceFactory(cache, opts), + await domainSettingsServiceFactory(cache, opts), await userVerificationServiceFactory(cache, opts), ), ); diff --git a/apps/browser/src/autofill/background/service_factories/domain-settings-service.factory.ts b/apps/browser/src/autofill/background/service_factories/domain-settings-service.factory.ts new file mode 100644 index 0000000000..1b4127c4cc --- /dev/null +++ b/apps/browser/src/autofill/background/service_factories/domain-settings-service.factory.ts @@ -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 { + return factory( + cache, + "domainSettingsService", + opts, + async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)), + ); +} diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index e8f77a6d47..f4422e6d7f 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -1,8 +1,8 @@ import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { UriMatchType } from "@bitwarden/common/vault/enums"; import { BrowserApi } from "../../platform/browser/browser-api"; @@ -73,7 +73,7 @@ export default class WebRequestBackground { const ciphers = await this.cipherService.getAllDecryptedForUrl( domain, null, - UriMatchType.Host, + UriMatchStrategy.Host, ); if (ciphers == null || ciphers.length !== 1) { error(); diff --git a/apps/browser/src/autofill/content/notification-bar.ts b/apps/browser/src/autofill/content/notification-bar.ts index f2a74310c2..8c1ef93c32 100644 --- a/apps/browser/src/autofill/content/notification-bar.ts +++ b/apps/browser/src/autofill/content/notification-bar.ts @@ -6,7 +6,7 @@ import AutofillField from "../models/autofill-field"; import { WatchedForm } from "../models/watched-form"; import { NotificationBarIframeInitData } from "../notification/abstractions/notification-bar"; import { FormData } from "../services/abstractions/autofill.service"; -import { GlobalSettings, UserSettings } from "../types"; +import { UserSettings } from "../types"; import { getFromLocalStorage, sendExtensionMessage, @@ -94,10 +94,11 @@ async function loadNotificationBar() { "bgGetEnableChangedPasswordPrompt", ); const enableAddedLoginPrompt = await sendExtensionMessage("bgGetEnableAddedLoginPrompt"); + const excludedDomains = await sendExtensionMessage("bgGetExcludedDomains"); + let showNotificationBar = true; // Look up the active user id from storage const activeUserIdKey = "activeUserId"; - const globalStorageKey = "global"; let activeUserId: string; const activeUserStorageValue = await getFromLocalStorage(activeUserIdKey); @@ -109,9 +110,6 @@ async function loadNotificationBar() { const userSettingsStorageValue = await getFromLocalStorage(activeUserId); if (userSettingsStorageValue[activeUserId]) { const userSettings: UserSettings = userSettingsStorageValue[activeUserId].settings; - const globalSettings: GlobalSettings = (await getFromLocalStorage(globalStorageKey))[ - globalStorageKey - ]; // Do not show the notification bar on the Bitwarden vault // 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). // It is managed in the Settings > Excluded Domains page in the browser extension. // 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 the user has not disabled both notifications, then handle the initial page change (null -> actual page) handlePageChange(); diff --git a/apps/browser/src/autofill/popup/settings/autofill.component.ts b/apps/browser/src/autofill/popup/settings/autofill.component.ts index 857442d309..67cc25f227 100644 --- a/apps/browser/src/autofill/popup/settings/autofill.component.ts +++ b/apps/browser/src/autofill/popup/settings/autofill.component.ts @@ -1,14 +1,16 @@ import { Component, OnInit } from "@angular/core"; import { firstValueFrom } from "rxjs"; -import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; 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 { + UriMatchStrategy, + UriMatchStrategySetting, +} from "@bitwarden/common/models/domain/domain-service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { BrowserApi } from "../../../platform/browser/browser-api"; @@ -28,16 +30,15 @@ export class AutofillComponent implements OnInit { enableAutoFillOnPageLoad = false; autoFillOnPageLoadDefault = false; autoFillOnPageLoadOptions: any[]; - defaultUriMatch = UriMatchType.Domain; + defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; uriMatchOptions: any[]; autofillKeyboardHelperText: string; accountSwitcherEnabled = false; constructor( - private stateService: StateService, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private autofillService: AutofillService, private dialogService: DialogService, private autofillSettingsService: AutofillSettingsServiceAbstraction, @@ -61,12 +62,12 @@ export class AutofillComponent implements OnInit { { name: i18nService.t("autoFillOnPageLoadNo"), value: false }, ]; this.uriMatchOptions = [ - { name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, - { name: i18nService.t("host"), value: UriMatchType.Host }, - { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchType.Exact }, - { name: i18nService.t("never"), value: UriMatchType.Never }, + { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, + { name: i18nService.t("host"), value: UriMatchStrategy.Host }, + { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, + { name: i18nService.t("never"), value: UriMatchStrategy.Never }, ]; this.accountSwitcherEnabled = enableAccountSwitching(); @@ -94,8 +95,10 @@ export class AutofillComponent implements OnInit { this.autofillSettingsService.autofillOnPageLoadDefault$, ); - const defaultUriMatch = await this.stateService.getDefaultUriMatch(); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; + const defaultUriMatch = await firstValueFrom( + this.domainSettingsService.defaultUriMatchStrategy$, + ); + this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; const command = await this.platformUtilsService.getAutofillKeyboardShortcut(); await this.setAutofillKeyboardHelperText(command); @@ -119,7 +122,7 @@ export class AutofillComponent implements OnInit { } async saveDefaultUriMatch() { - await this.stateService.setDefaultUriMatch(this.defaultUriMatch); + await this.domainSettingsService.setDefaultUriMatchStrategy(this.defaultUriMatch); } private async setAutofillKeyboardHelperText(command: string) { diff --git a/apps/browser/src/autofill/services/abstractions/autofill.service.ts b/apps/browser/src/autofill/services/abstractions/autofill.service.ts index c44e3adf7c..77a5f982fd 100644 --- a/apps/browser/src/autofill/services/abstractions/autofill.service.ts +++ b/apps/browser/src/autofill/services/abstractions/autofill.service.ts @@ -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 AutofillField from "../../models/autofill-field"; @@ -40,7 +41,7 @@ export interface GenerateFillScriptOptions { allowTotpAutofill: boolean; cipher: CipherView; tabUrl: string; - defaultUriMatch: UriMatchType; + defaultUriMatch: UriMatchStrategySetting; } export abstract class AutofillService { diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 4be338da8e..2a9519292e 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -1,19 +1,25 @@ import { mock, mockReset } from "jest-mock-extended"; +import { of } from "rxjs"; import { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.service"; import { AutofillOverlayVisibility } from "@bitwarden/common/autofill/constants"; 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 { - FieldType, - LinkedIdType, - LoginLinkedId, - UriMatchType, - CipherType, -} from "@bitwarden/common/vault/enums"; + DefaultDomainSettingsService, + DomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; +import { EventType } from "@bitwarden/common/enums"; +import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; +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 { CardView } from "@bitwarden/common/vault/models/view/card.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -47,15 +53,24 @@ import { import { AutoFillConstants, IdentityAutoFillConstants } from "./autofill-constants"; 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", () => { let autofillService: AutofillService; const cipherService = mock(); const stateService = mock(); const autofillSettingsService = mock(); + const mockUserId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(mockUserId); + const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService); + let domainSettingsService: DomainSettingsService; const totpService = mock(); const eventCollectionService = mock(); const logService = mock(); - const settingsService = mock(); const userVerificationService = mock(); beforeEach(() => { @@ -66,9 +81,12 @@ describe("AutofillService", () => { totpService, eventCollectionService, logService, - settingsService, + domainSettingsService, userVerificationService, ); + + domainSettingsService = new DefaultDomainSettingsService(fakeStateProvider); + domainSettingsService.equivalentDomains$ = of(mockEquivalentDomains); }); afterEach(() => { @@ -407,6 +425,8 @@ describe("AutofillService", () => { autofillOptions.cipher.login.matchesUri = jest.fn().mockReturnValue(true); autofillOptions.cipher.login.username = "username"; autofillOptions.cipher.login.password = "password"; + + jest.spyOn(autofillService, "getDefaultUriMatchStrategy").mockResolvedValue(0); }); 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 () => { jest.spyOn(stateService, "getCanAccessPremium"); - jest.spyOn(stateService, "getDefaultUriMatch"); jest.spyOn(autofillService as any, "generateFillScript"); jest.spyOn(autofillService as any, "generateLoginFillScript"); jest.spyOn(logService, "info"); @@ -479,7 +498,7 @@ describe("AutofillService", () => { const currentAutofillPageDetails = autofillOptions.pageDetails[0]; expect(stateService.getCanAccessPremium).toHaveBeenCalled(); - expect(stateService.getDefaultUriMatch).toHaveBeenCalled(); + expect(autofillService["getDefaultUriMatchStrategy"]).toHaveBeenCalled(); expect(autofillService["generateFillScript"]).toHaveBeenCalledWith( currentAutofillPageDetails.details, { @@ -1488,7 +1507,7 @@ describe("AutofillService", () => { }; defaultLoginUriView = mock({ uri: "https://www.example.com", - match: UriMatchType.Domain, + match: UriMatchStrategy.Domain, }); options = createGenerateFillScriptOptionsMock(); options.cipher.login = mock({ @@ -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({ uri: "https://www.second-example.com", }); const thirdUriView = mock({ uri: "https://www.third-example.com", - match: UriMatchType.Never, + match: UriMatchStrategy.Never, }); options.cipher.login.uris = [defaultLoginUriView, secondUriView, thirdUriView]; @@ -2752,31 +2771,32 @@ describe("AutofillService", () => { }); 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 tabUrl = "https://www.example.com"; const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); 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(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 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 }); 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( pageUrl, equivalentDomains, @@ -2785,17 +2805,21 @@ describe("AutofillService", () => { 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 tabUrl = "https://www.not-example.com"; - const equivalentDomains = new Set(["not-example.com"]); const generateFillScriptOptions = createGenerateFillScriptOptionsMock({ tabUrl }); 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( pageUrl, equivalentDomains, diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 4357a9cb6c..3cb73dd72b 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -1,15 +1,19 @@ import { firstValueFrom } from "rxjs"; 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 { 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 { 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.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 eventCollectionService: EventCollectionService, private logService: LogService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private userVerificationService: UserVerificationService, ) {} @@ -215,6 +219,13 @@ export default class AutofillService implements AutofillServiceInterface { return await firstValueFrom(this.autofillSettingsService.autofillOnPageLoad$); } + /** + * Gets the default URI match strategy setting from the domain settings service. + */ + async getDefaultUriMatchStrategy(): Promise { + return await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); + } + /** * Autofill a given tab with a given 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; const canAccessPremium = await this.stateService.getCanAccessPremium(); - const defaultUriMatch = (await this.stateService.getDefaultUriMatch()) ?? UriMatchType.Domain; + const defaultUriMatch = await this.getDefaultUriMatchStrategy(); if (!canAccessPremium) { options.cipher.login.totp = null; @@ -579,9 +590,9 @@ export default class AutofillService implements AutofillServiceInterface { let totp: AutofillField = null; const login = options.cipher.login; 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( 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 * @private */ - private inUntrustedIframe(pageUrl: string, options: GenerateFillScriptOptions): boolean { + private async inUntrustedIframe( + pageUrl: string, + options: GenerateFillScriptOptions, + ): Promise { // 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 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. // 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. - const equivalentDomains = this.settingsService.getEquivalentDomains(pageUrl); + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(pageUrl), + ); const matchesUri = options.cipher.login.matchesUri( pageUrl, equivalentDomains, diff --git a/apps/browser/src/autofill/spec/autofill-mocks.ts b/apps/browser/src/autofill/spec/autofill-mocks.ts index b580977a37..708489c57e 100644 --- a/apps/browser/src/autofill/spec/autofill-mocks.ts +++ b/apps/browser/src/autofill/spec/autofill-mocks.ts @@ -1,8 +1,9 @@ import { mock } from "jest-mock-extended"; 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 { 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -112,7 +113,7 @@ function createGenerateFillScriptOptionsMock(customFields = {}): GenerateFillScr allowTotpAutofill: false, cipher: mock(), tabUrl: "https://jest-testing-website.com", - defaultUriMatch: UriMatchType.Domain, + defaultUriMatch: UriMatchStrategy.Domain, ...customFields, }; } diff --git a/apps/browser/src/autofill/types/index.ts b/apps/browser/src/autofill/types/index.ts index d9ae0b16f4..8ed893e733 100644 --- a/apps/browser/src/autofill/types/index.ts +++ b/apps/browser/src/autofill/types/index.ts @@ -1,5 +1,4 @@ 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 { CipherType } from "@bitwarden/common/vault/enums"; @@ -32,15 +31,10 @@ export type UserSettings = { utcDate: string; version: string; }; - settings: { - equivalentDomains: string[][]; - }; vaultTimeout: number; vaultTimeoutAction: VaultTimeoutAction; }; -export type GlobalSettings = Pick; - /** * A HTMLElement (usually a form element) with additional custom properties added by this script */ diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 42790dea3a..0f4ddd8a20 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -56,6 +56,10 @@ import { BadgeSettingsServiceAbstraction, BadgeSettingsService, } from "@bitwarden/common/autofill/services/badge-settings.service"; +import { + DomainSettingsService, + DefaultDomainSettingsService, +} from "@bitwarden/common/autofill/services/domain-settings.service"; import { UserNotificationSettingsService, UserNotificationSettingsServiceAbstraction, @@ -259,6 +263,7 @@ export default class MainBackground { userNotificationSettingsService: UserNotificationSettingsServiceAbstraction; autofillSettingsService: AutofillSettingsServiceAbstraction; badgeSettingsService: BadgeSettingsServiceAbstraction; + domainSettingsService: DomainSettingsService; systemService: SystemServiceAbstraction; eventCollectionService: EventCollectionServiceAbstraction; eventUploadService: EventUploadServiceAbstraction; @@ -466,6 +471,7 @@ export default class MainBackground { this.appIdService, (expired: boolean) => this.logout(expired), ); + this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); this.settingsService = new BrowserSettingsService(this.stateService); this.fileUploadService = new FileUploadService(this.logService); this.cipherFileUploadService = new CipherFileUploadService( @@ -591,7 +597,7 @@ export default class MainBackground { this.cipherService = new CipherService( this.cryptoService, - this.settingsService, + this.domainSettingsService, this.apiService, this.i18nService, this.searchService, @@ -678,7 +684,7 @@ export default class MainBackground { this.providerService = new ProviderService(this.stateProvider); this.syncService = new SyncService( this.apiService, - this.settingsService, + this.domainSettingsService, this.folderService, this.cipherService, this.cryptoService, @@ -715,7 +721,7 @@ export default class MainBackground { this.totpService, this.eventCollectionService, this.logService, - this.settingsService, + this.domainSettingsService, this.userVerificationService, ); this.auditService = new AuditService(this.cryptoFunctionService, this.apiService); @@ -779,6 +785,7 @@ export default class MainBackground { this.authService, this.stateService, this.vaultSettingsService, + this.domainSettingsService, this.logService, ); @@ -848,6 +855,7 @@ export default class MainBackground { this.folderService, this.stateService, this.userNotificationSettingsService, + this.domainSettingsService, this.environmentService, this.logService, ); @@ -1081,7 +1089,6 @@ export default class MainBackground { await Promise.all([ this.syncService.setLastSync(new Date(0), userId), this.cryptoService.clearKeys(userId), - this.settingsService.clear(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), diff --git a/apps/browser/src/popup/settings/excluded-domains.component.ts b/apps/browser/src/popup/settings/excluded-domains.component.ts index a7708c7ce3..6fa28d10ab 100644 --- a/apps/browser/src/popup/settings/excluded-domains.component.ts +++ b/apps/browser/src/popup/settings/excluded-domains.component.ts @@ -1,6 +1,8 @@ import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -30,6 +32,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy { constructor( private stateService: StateService, + private domainSettingsService: DomainSettingsService, private i18nService: I18nService, private router: Router, private broadcasterService: BroadcasterService, @@ -40,7 +43,7 @@ export class ExcludedDomainsComponent implements OnInit, OnDestroy { } async ngOnInit() { - const savedDomains = await this.stateService.getNeverDomains(); + const savedDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); if (savedDomains) { for (const uri of Object.keys(savedDomains)) { 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. // eslint-disable-next-line @typescript-eslint/no-floating-promises this.router.navigate(["/tabs/settings"]); diff --git a/apps/browser/src/popup/settings/options.component.ts b/apps/browser/src/popup/settings/options.component.ts index 2af3713171..eca37e3315 100644 --- a/apps/browser/src/popup/settings/options.component.ts +++ b/apps/browser/src/popup/settings/options.component.ts @@ -5,14 +5,18 @@ import { AbstractThemingService } from "@bitwarden/angular/platform/services/the import { SettingsService } from "@bitwarden/common/abstractions/settings.service"; import { AutofillSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/autofill-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 { 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ThemeType } from "@bitwarden/common/platform/enums"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; -import { UriMatchType } from "@bitwarden/common/vault/enums"; import { enableAccountSwitching } from "../../platform/flags"; @@ -36,7 +40,7 @@ export class OptionsComponent implements OnInit { showClearClipboard = true; theme: ThemeType; themeOptions: any[]; - defaultUriMatch = UriMatchType.Domain; + defaultUriMatch: UriMatchStrategySetting = UriMatchStrategy.Domain; uriMatchOptions: any[]; clearClipboard: ClearClipboardDelaySetting; clearClipboardOptions: any[]; @@ -50,6 +54,7 @@ export class OptionsComponent implements OnInit { private stateService: StateService, private userNotificationSettingsService: UserNotificationSettingsServiceAbstraction, private autofillSettingsService: AutofillSettingsServiceAbstraction, + private domainSettingsService: DomainSettingsService, private badgeSettingsService: BadgeSettingsServiceAbstraction, i18nService: I18nService, private themingService: AbstractThemingService, @@ -64,12 +69,12 @@ export class OptionsComponent implements OnInit { { name: i18nService.t("solarizedDark"), value: ThemeType.SolarizedDark }, ]; this.uriMatchOptions = [ - { name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, - { name: i18nService.t("host"), value: UriMatchType.Host }, - { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchType.Exact }, - { name: i18nService.t("never"), value: UriMatchType.Never }, + { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, + { name: i18nService.t("host"), value: UriMatchStrategy.Host }, + { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, + { name: i18nService.t("never"), value: UriMatchStrategy.Never }, ]; this.clearClipboardOptions = [ { name: i18nService.t("never"), value: null }, @@ -122,8 +127,10 @@ export class OptionsComponent implements OnInit { this.theme = await this.stateService.getTheme(); - const defaultUriMatch = await this.stateService.getDefaultUriMatch(); - this.defaultUriMatch = defaultUriMatch == null ? UriMatchType.Domain : defaultUriMatch; + const defaultUriMatch = await firstValueFrom( + this.domainSettingsService.defaultUriMatchStrategy$, + ); + this.defaultUriMatch = defaultUriMatch == null ? UriMatchStrategy.Domain : defaultUriMatch; this.clearClipboard = await firstValueFrom(this.autofillSettingsService.clearClipboardDelay$); } @@ -182,10 +189,6 @@ export class OptionsComponent implements OnInit { await this.themingService.updateConfiguredTheme(this.theme); } - async saveDefaultUriMatch() { - await this.stateService.setDefaultUriMatch(this.defaultUriMatch); - } - async saveClearClipboard() { await this.autofillSettingsService.setClearClipboardDelay(this.clearClipboard); } diff --git a/apps/browser/src/services/browser-settings.service.ts b/apps/browser/src/services/browser-settings.service.ts index 89378bcc74..50c27ce4f6 100644 --- a/apps/browser/src/services/browser-settings.service.ts +++ b/apps/browser/src/services/browser-settings.service.ts @@ -1,15 +1,11 @@ import { BehaviorSubject } from "rxjs"; -import { AccountSettingsSettings } from "@bitwarden/common/platform/models/domain/account"; import { SettingsService } from "@bitwarden/common/services/settings.service"; import { browserSession, sessionSync } from "../platform/decorators/session-sync-observable"; @browserSession export class BrowserSettingsService extends SettingsService { - @sessionSync({ initializer: (obj: string[][]) => obj }) - protected _settings: BehaviorSubject; - @sessionSync({ initializer: (b: boolean) => b }) protected _disableFavicon: BehaviorSubject; } diff --git a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts index 2a0821d0d1..8ffeca72bc 100644 --- a/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts +++ b/apps/browser/src/vault/background/service_factories/cipher-service.factory.ts @@ -5,6 +5,10 @@ import { AutofillSettingsServiceInitOptions, autofillSettingsServiceFactory, } from "../../../autofill/background/service_factories/autofill-settings-service.factory"; +import { + DomainSettingsServiceInitOptions, + domainSettingsServiceFactory, +} from "../../../autofill/background/service_factories/domain-settings-service.factory"; import { CipherFileUploadServiceInitOptions, cipherFileUploadServiceFactory, @@ -13,10 +17,6 @@ import { searchServiceFactory, SearchServiceInitOptions, } from "../../../background/service-factories/search-service.factory"; -import { - SettingsServiceInitOptions, - settingsServiceFactory, -} from "../../../background/service-factories/settings-service.factory"; import { apiServiceFactory, ApiServiceInitOptions, @@ -51,13 +51,13 @@ type CipherServiceFactoryOptions = FactoryOptions; export type CipherServiceInitOptions = CipherServiceFactoryOptions & CryptoServiceInitOptions & - SettingsServiceInitOptions & ApiServiceInitOptions & CipherFileUploadServiceInitOptions & I18nServiceInitOptions & SearchServiceInitOptions & StateServiceInitOptions & AutofillSettingsServiceInitOptions & + DomainSettingsServiceInitOptions & EncryptServiceInitOptions & ConfigServiceInitOptions; @@ -72,7 +72,7 @@ export function cipherServiceFactory( async () => new CipherService( await cryptoServiceFactory(cache, opts), - await settingsServiceFactory(cache, opts), + await domainSettingsServiceFactory(cache, opts), await apiServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await searchServiceFactory(cache, opts), diff --git a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts index ce22787f09..9ea0914d87 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2-use-browser-link.component.ts @@ -3,6 +3,7 @@ import { ConnectedPosition } from "@angular/cdk/overlay"; import { Component } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @@ -52,6 +53,7 @@ export class Fido2UseBrowserLinkComponent { constructor( private stateService: StateService, + private domainSettingsService: DomainSettingsService, private platformUtilsService: PlatformUtilsService, private i18nService: I18nService, ) {} @@ -89,7 +91,7 @@ export class Fido2UseBrowserLinkComponent { * @param uri - The domain uri to exclude from future FIDO2 prompts. */ private async handleDomainExclusion(uri: string) { - const exisitingDomains = await this.stateService.getNeverDomains(); + const exisitingDomains = await firstValueFrom(this.domainSettingsService.neverDomains$); const validDomain = Utils.getHostname(uri); const savedDomains: { [name: string]: unknown } = { @@ -97,9 +99,7 @@ export class Fido2UseBrowserLinkComponent { }; 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. - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.stateService.setNeverDomains(savedDomains); + await this.domainSettingsService.setNeverDomains(savedDomains); this.platformUtilsService.showToast( "success", diff --git a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts index 6cd5046826..81d1b88fd8 100644 --- a/apps/browser/src/vault/popup/components/fido2/fido2.component.ts +++ b/apps/browser/src/vault/popup/components/fido2/fido2.component.ts @@ -5,6 +5,7 @@ import { combineLatest, concatMap, filter, + firstValueFrom, map, Observable, Subject, @@ -13,7 +14,7 @@ import { } from "rxjs"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -72,7 +73,7 @@ export class Fido2Component implements OnInit, OnDestroy { private cipherService: CipherService, private passwordRepromptService: PasswordRepromptService, private platformUtilsService: PlatformUtilsService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private searchService: SearchService, private logService: LogService, private dialogService: DialogService, @@ -133,7 +134,9 @@ export class Fido2Component implements OnInit, OnDestroy { concatMap(async (message) => { switch (message.type) { 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( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, @@ -317,7 +320,9 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers, ); } else { - const equivalentDomains = this.settingsService.getEquivalentDomains(this.url); + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(this.url), + ); this.displayedCiphers = this.ciphers.filter((cipher) => cipher.login.matchesUri(this.url, equivalentDomains), ); diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index eb02abd8ba..031be3b48c 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -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 { UserVerificationService } from "@bitwarden/common/auth/services/user-verification/user-verification.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 { ConfigApiServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config-api.service.abstraction"; import { KeyGenerationService as KeyGenerationServiceAbstraction } from "@bitwarden/common/platform/abstractions/key-generation.service"; @@ -190,6 +194,7 @@ export class Main { pinCryptoService: PinCryptoServiceAbstraction; stateService: StateService; autofillSettingsService: AutofillSettingsServiceAbstraction; + domainSettingsService: DomainSettingsService; organizationService: OrganizationService; providerService: ProviderService; twoFactorService: TwoFactorService; @@ -358,6 +363,7 @@ export class Main { this.containerService = new ContainerService(this.cryptoService, this.encryptService); this.settingsService = new SettingsService(this.stateService); + this.domainSettingsService = new DefaultDomainSettingsService(this.stateProvider); this.fileUploadService = new FileUploadService(this.logService); @@ -481,7 +487,7 @@ export class Main { this.cipherService = new CipherService( this.cryptoService, - this.settingsService, + this.domainSettingsService, this.apiService, this.i18nService, this.searchService, @@ -551,7 +557,7 @@ export class Main { this.syncService = new SyncService( this.apiService, - this.settingsService, + this.domainSettingsService, this.folderService, this.cipherService, this.cryptoService, @@ -647,7 +653,6 @@ export class Main { await Promise.all([ this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), - this.settingsService.clear(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId as UserId), diff --git a/apps/desktop/src/app/app.component.ts b/apps/desktop/src/app/app.component.ts index cbee441fa3..a59b07241d 100644 --- a/apps/desktop/src/app/app.component.ts +++ b/apps/desktop/src/app/app.component.ts @@ -577,7 +577,6 @@ export class AppComponent implements OnInit, OnDestroy { await this.eventUploadService.uploadEvents(userBeingLoggedOut); await this.syncService.setLastSync(new Date(0), userBeingLoggedOut); await this.cryptoService.clearKeys(userBeingLoggedOut); - await this.settingsService.clear(userBeingLoggedOut); await this.cipherService.clear(userBeingLoggedOut); await this.folderService.clear(userBeingLoggedOut); await this.collectionService.clear(userBeingLoggedOut); diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 73721de789..83900cd85d 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -275,7 +275,6 @@ export class AppComponent implements OnDestroy, OnInit { await Promise.all([ this.syncService.setLastSync(new Date(0)), this.cryptoService.clearKeys(), - this.settingsService.clear(userId), this.cipherService.clear(userId), this.folderService.clear(userId), this.collectionService.clear(userId), diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index eeea73e465..b4d657c027 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -90,6 +90,10 @@ import { BadgeSettingsServiceAbstraction, BadgeSettingsService, } 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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions/organization-billing.service"; import { PaymentMethodWarningsServiceAbstraction } from "@bitwarden/common/billing/abstractions/payment-method-warnings-service.abstraction"; @@ -351,13 +355,14 @@ import { ModalService } from "./modal.service"; searchService: SearchServiceAbstraction, stateService: StateServiceAbstraction, autofillSettingsService: AutofillSettingsServiceAbstraction, + domainSettingsService: DomainSettingsService, encryptService: EncryptService, fileUploadService: CipherFileUploadServiceAbstraction, configService: ConfigServiceAbstraction, ) => new CipherService( cryptoService, - settingsService, + domainSettingsService, apiService, i18nService, searchService, @@ -962,6 +967,11 @@ import { ModalService } from "./modal.service"; useClass: BadgeSettingsService, deps: [StateProvider], }, + { + provide: DomainSettingsService, + useClass: DefaultDomainSettingsService, + deps: [StateProvider], + }, { provide: BiometricStateService, useClass: DefaultBiometricStateService, diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 2c216ad5ca..680672514a 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -13,6 +13,7 @@ import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin- import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { EventType } from "@bitwarden/common/enums"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.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 { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; 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 { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CardView } from "@bitwarden/common/vault/models/view/card.view"; @@ -164,12 +165,12 @@ export class AddEditComponent implements OnInit, OnDestroy { ]; this.uriMatchOptions = [ { name: i18nService.t("defaultMatchDetection"), value: null }, - { name: i18nService.t("baseDomain"), value: UriMatchType.Domain }, - { name: i18nService.t("host"), value: UriMatchType.Host }, - { name: i18nService.t("startsWith"), value: UriMatchType.StartsWith }, - { name: i18nService.t("regEx"), value: UriMatchType.RegularExpression }, - { name: i18nService.t("exact"), value: UriMatchType.Exact }, - { name: i18nService.t("never"), value: UriMatchType.Never }, + { name: i18nService.t("baseDomain"), value: UriMatchStrategy.Domain }, + { name: i18nService.t("host"), value: UriMatchStrategy.Host }, + { name: i18nService.t("startsWith"), value: UriMatchStrategy.StartsWith }, + { name: i18nService.t("regEx"), value: UriMatchStrategy.RegularExpression }, + { name: i18nService.t("exact"), value: UriMatchStrategy.Exact }, + { name: i18nService.t("never"), value: UriMatchStrategy.Never }, ]; this.autofillOnPageLoadOptions = [ { name: i18nService.t("autoFillOnPageLoadUseDefault"), value: null }, diff --git a/libs/common/src/abstractions/settings.service.ts b/libs/common/src/abstractions/settings.service.ts index 78ed7183c8..e9d8c8b683 100644 --- a/libs/common/src/abstractions/settings.service.ts +++ b/libs/common/src/abstractions/settings.service.ts @@ -1,14 +1,8 @@ import { Observable } from "rxjs"; -import { AccountSettingsSettings } from "../platform/models/domain/account"; - export abstract class SettingsService { - settings$: Observable; disableFavicon$: Observable; - setEquivalentDomains: (equivalentDomains: string[][]) => Promise; - getEquivalentDomains: (url: string) => Set; setDisableFavicon: (value: boolean) => Promise; getDisableFavicon: () => boolean; - clear: (userId?: string) => Promise; } diff --git a/libs/common/src/autofill/services/domain-settings.service.spec.ts b/libs/common/src/autofill/services/domain-settings.service.spec.ts new file mode 100644 index 0000000000..24e3763eb4 --- /dev/null +++ b/libs/common/src/autofill/services/domain-settings.service.spec.ts @@ -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()); + }); + }); +}); diff --git a/libs/common/src/autofill/services/domain-settings.service.ts b/libs/common/src/autofill/services/domain-settings.service.ts new file mode 100644 index 0000000000..3131b9c50b --- /dev/null +++ b/libs/common/src/autofill/services/domain-settings.service.ts @@ -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; + setNeverDomains: (newValue: NeverDomains) => Promise; + equivalentDomains$: Observable; + setEquivalentDomains: (newValue: EquivalentDomains) => Promise; + defaultUriMatchStrategy$: Observable; + setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise; + getUrlEquivalentDomains: (url: string) => Observable>; +} + +export class DefaultDomainSettingsService implements DomainSettingsService { + private neverDomainsState: GlobalState; + readonly neverDomains$: Observable; + + private equivalentDomainsState: ActiveUserState; + readonly equivalentDomains$: Observable; + + private defaultUriMatchStrategyState: ActiveUserState; + readonly defaultUriMatchStrategy$: Observable; + + 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 { + await this.neverDomainsState.update(() => newValue); + } + + async setEquivalentDomains(newValue: EquivalentDomains): Promise { + await this.equivalentDomainsState.update(() => newValue); + } + + async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise { + await this.defaultUriMatchStrategyState.update(() => newValue); + } + + getUrlEquivalentDomains(url: string): Observable> { + const domains$ = this.equivalentDomains$.pipe( + map((equivalentDomains) => { + const domain = Utils.getDomain(url); + if (domain == null || equivalentDomains == null) { + return new Set() as Set; + } + + const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat(); + + return new Set(equivalents); + }), + ); + + return domains$; + } +} diff --git a/libs/common/src/models/domain/domain-service.ts b/libs/common/src/models/domain/domain-service.ts new file mode 100644 index 0000000000..d5247765fc --- /dev/null +++ b/libs/common/src/models/domain/domain-service.ts @@ -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[][]; diff --git a/libs/common/src/models/export/login-uri.export.ts b/libs/common/src/models/export/login-uri.export.ts index 8a21c3ff97..83a7d25eff 100644 --- a/libs/common/src/models/export/login-uri.export.ts +++ b/libs/common/src/models/export/login-uri.export.ts @@ -1,5 +1,5 @@ +import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { EncString } from "../../platform/models/domain/enc-string"; -import { UriMatchType } from "../../vault/enums"; import { LoginUri as LoginUriDomain } from "../../vault/models/domain/login-uri"; import { LoginUriView } from "../../vault/models/view/login-uri.view"; @@ -26,7 +26,7 @@ export class LoginUriExport { uri: string; uriChecksum: string | undefined; - match: UriMatchType = null; + match: UriMatchStrategySetting = null; constructor(o?: LoginUriView | LoginUriDomain) { if (o == null) { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 2548913555..a1dbd52d3b 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -14,18 +14,13 @@ import { SendData } from "../../tools/send/models/data/send.data"; import { SendView } from "../../tools/send/models/view/send.view"; import { UserId } from "../../types/guid"; import { DeviceKey, MasterKey } from "../../types/key"; -import { UriMatchType } from "../../vault/enums"; import { CipherData } from "../../vault/models/data/cipher.data"; import { LocalData } from "../../vault/models/data/local.data"; import { CipherView } from "../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType, ThemeType } from "../enums"; import { ServerConfigData } from "../models/data/server-config.data"; -import { - Account, - AccountDecryptionOptions, - AccountSettingsSettings, -} from "../models/domain/account"; +import { Account, AccountDecryptionOptions } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { StorageOptions } from "../models/domain/storage-options"; import { SymmetricCryptoKey } from "../models/domain/symmetric-crypto-key"; @@ -187,8 +182,6 @@ export abstract class StateService { * @deprecated Do not call this directly, use SendService */ setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise; - getDefaultUriMatch: (options?: StorageOptions) => Promise; - setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise; /** * @deprecated Do not call this, use SettingsService */ @@ -275,8 +268,6 @@ export abstract class StateService { * @deprecated Do not call this directly, use SendService */ setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise; - getEquivalentDomains: (options?: StorageOptions) => Promise; - setEquivalentDomains: (value: string, options?: StorageOptions) => Promise; getEventCollection: (options?: StorageOptions) => Promise; setEventCollection: (value: EventData[], options?: StorageOptions) => Promise; getEverBeenUnlocked: (options?: StorageOptions) => Promise; @@ -310,8 +301,6 @@ export abstract class StateService { setMainWindowSize: (value: number, options?: StorageOptions) => Promise; getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise; setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise; - getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>; - setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise; getOpenAtLogin: (options?: StorageOptions) => Promise; setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise; getOrganizationInvitation: (options?: StorageOptions) => Promise; @@ -353,14 +342,6 @@ export abstract class StateService { setRememberedEmail: (value: string, options?: StorageOptions) => Promise; getSecurityStamp: (options?: StorageOptions) => Promise; setSecurityStamp: (value: string, options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use SettingsService - */ - getSettings: (options?: StorageOptions) => Promise; - /** - * @deprecated Do not call this directly, use SettingsService - */ - setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise; getTheme: (options?: StorageOptions) => Promise; setTheme: (value: ThemeType, options?: StorageOptions) => Promise; getTwoFactorToken: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 32b1de9c8f..2c3c2eab67 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -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 { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { EventData } from "../../../models/data/event.data"; +import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { GeneratedPasswordHistory, @@ -17,7 +18,6 @@ import { SendData } from "../../../tools/send/models/data/send.data"; import { SendView } from "../../../tools/send/models/view/send.view"; import { DeepJsonify } from "../../../types/deep-jsonify"; import { MasterKey } from "../../../types/key"; -import { UriMatchType } from "../../../vault/enums"; import { CipherData } from "../../../vault/models/data/cipher.data"; import { CipherView } from "../../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../../vault/types/add-edit-cipher-info"; @@ -196,13 +196,12 @@ export class AccountProfile { export class AccountSettings { autoConfirmFingerPrints?: boolean; - defaultUriMatch?: UriMatchType; + defaultUriMatch?: UriMatchStrategySetting; disableGa?: boolean; dontShowCardsCurrentTab?: boolean; dontShowIdentitiesCurrentTab?: boolean; enableAlwaysOnTop?: boolean; enableBiometric?: boolean; - equivalentDomains?: any; minimizeOnCopyToClipboard?: boolean; passwordGenerationOptions?: PasswordGeneratorOptions; usernameGenerationOptions?: UsernameGeneratorOptions; @@ -210,7 +209,6 @@ export class AccountSettings { pinKeyEncryptedUserKey?: EncryptedString; pinKeyEncryptedUserKeyEphemeral?: EncryptedString; protectedPin?: string; - settings?: AccountSettingsSettings; // TODO: Merge whatever is going on here into the AccountSettings model properly vaultTimeout?: number; vaultTimeoutAction?: string = "lock"; serverConfig?: ServerConfigData; @@ -236,10 +234,6 @@ export class AccountSettings { } } -export type AccountSettingsSettings = { - equivalentDomains?: string[][]; -}; - export class AccountTokens { accessToken?: string; refreshToken?: string; diff --git a/libs/common/src/platform/models/domain/global-state.ts b/libs/common/src/platform/models/domain/global-state.ts index b27482fa6a..0b018aa36b 100644 --- a/libs/common/src/platform/models/domain/global-state.ts +++ b/libs/common/src/platform/models/domain/global-state.ts @@ -25,6 +25,5 @@ export class GlobalState { enableBrowserIntegration?: boolean; enableBrowserIntegrationFingerprint?: boolean; enableDuckDuckGoBrowserIntegration?: boolean; - neverDomains?: { [id: string]: unknown }; deepLinkRedirectUrl?: string; } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 9313af551b..53a2a92f97 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -18,7 +18,6 @@ import { SendData } from "../../tools/send/models/data/send.data"; import { SendView } from "../../tools/send/models/view/send.view"; import { UserId } from "../../types/guid"; import { DeviceKey, MasterKey } from "../../types/key"; -import { UriMatchType } from "../../vault/enums"; import { CipherData } from "../../vault/models/data/cipher.data"; import { LocalData } from "../../vault/models/data/local.data"; import { CipherView } from "../../vault/models/view/cipher.view"; @@ -42,7 +41,6 @@ import { AccountData, AccountDecryptionOptions, AccountSettings, - AccountSettingsSettings, } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; @@ -815,23 +813,6 @@ export class StateService< ); } - async getDefaultUriMatch(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.defaultUriMatch; - } - - async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise { - 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 { return ( ( @@ -1333,23 +1314,6 @@ export class StateService< ); } - async getEquivalentDomains(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions())) - )?.settings?.equivalentDomains; - } - - async setEquivalentDomains(value: string, options?: StorageOptions): Promise { - 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) async getEventCollection(options?: StorageOptions): Promise { 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 { - 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 { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) @@ -1807,23 +1754,6 @@ export class StateService< ); } - async getSettings(options?: StorageOptions): Promise { - return ( - await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions())) - )?.settings?.settings; - } - - async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise { - 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 { return ( await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index bcae7de9ad..9eb8e6d669 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -39,6 +39,8 @@ export const USER_NOTIFICATION_SETTINGS_DISK = new StateDefinition( // Billing +export const DOMAIN_SETTINGS_DISK = new StateDefinition("domainSettings", "disk"); + export const AUTOFILL_SETTINGS_DISK = new StateDefinition("autofillSettings", "disk"); export const AUTOFILL_SETTINGS_DISK_LOCAL = new StateDefinition("autofillSettingsLocal", "disk", { web: "disk-local", diff --git a/libs/common/src/services/search.service.ts b/libs/common/src/services/search.service.ts index 467a890950..773d51297a 100644 --- a/libs/common/src/services/search.service.ts +++ b/libs/common/src/services/search.service.ts @@ -1,10 +1,11 @@ import * as lunr from "lunr"; import { SearchService as SearchServiceAbstraction } from "../abstractions/search.service"; +import { UriMatchStrategy } from "../models/domain/domain-service"; import { I18nService } from "../platform/abstractions/i18n.service"; import { LogService } from "../platform/abstractions/log.service"; 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 { CipherView } from "../vault/models/view/cipher.view"; @@ -288,7 +289,7 @@ export class SearchService implements SearchServiceAbstraction { return; } let uri = u.uri; - if (u.match !== UriMatchType.RegularExpression) { + if (u.match !== UriMatchStrategy.RegularExpression) { const protocolIndex = uri.indexOf("://"); if (protocolIndex > -1) { uri = uri.substr(protocolIndex + 3); diff --git a/libs/common/src/services/settings.service.spec.ts b/libs/common/src/services/settings.service.spec.ts deleted file mode 100644 index 6f5aced590..0000000000 --- a/libs/common/src/services/settings.service.spec.ts +++ /dev/null @@ -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; - let encryptService: MockProxy; - let stateService: MockProxy; - let activeAccount: BehaviorSubject; - let activeAccountUnlocked: BehaviorSubject; - - 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(); - encryptService = mock(); - stateService = mock(); - 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({}); - }); -}); diff --git a/libs/common/src/services/settings.service.ts b/libs/common/src/services/settings.service.ts index d20efc80c1..9a4d04a147 100644 --- a/libs/common/src/services/settings.service.ts +++ b/libs/common/src/services/settings.service.ts @@ -3,13 +3,10 @@ import { BehaviorSubject, concatMap } from "rxjs"; import { SettingsService as SettingsServiceAbstraction } from "../abstractions/settings.service"; import { StateService } from "../platform/abstractions/state.service"; import { Utils } from "../platform/misc/utils"; -import { AccountSettingsSettings } from "../platform/models/domain/account"; export class SettingsService implements SettingsServiceAbstraction { - protected _settings: BehaviorSubject = new BehaviorSubject({}); protected _disableFavicon = new BehaviorSubject(null); - settings$ = this._settings.asObservable(); disableFavicon$ = this._disableFavicon.asObservable(); constructor(private stateService: StateService) { @@ -21,50 +18,17 @@ export class SettingsService implements SettingsServiceAbstraction { } if (!unlocked) { - this._settings.next({}); return; } - const data = await this.stateService.getSettings(); const disableFavicon = await this.stateService.getDisableFavicon(); - this._settings.next(data); this._disableFavicon.next(disableFavicon); }), ) .subscribe(); } - async setEquivalentDomains(equivalentDomains: string[][]): Promise { - const settings = this._settings.getValue() ?? {}; - - settings.equivalentDomains = equivalentDomains; - - this._settings.next(settings); - await this.stateService.setSettings(settings); - } - - getEquivalentDomains(url: string): Set { - 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) { this._disableFavicon.next(value); await this.stateService.setDisableFavicon(value); @@ -73,12 +37,4 @@ export class SettingsService implements SettingsServiceAbstraction { getDisableFavicon(): boolean { return this._disableFavicon.getValue(); } - - async clear(userId?: string): Promise { - if (userId == null || userId == (await this.stateService.getUserId())) { - this._settings.next({}); - } - - await this.stateService.setSettings(null, { userId: userId }); - } } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 47fbf12575..5064968ef5 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -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 { PreferredLanguageMigrator } from "./migrations/32-move-preferred-language"; 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 { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; 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"; export const MIN_VERSION = 2; -export const CURRENT_VERSION = 33; +export const CURRENT_VERSION = 34; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -74,7 +75,8 @@ export function createMigrationBuilder() { .with(PolicyMigrator, 29, 30) .with(EnableContextMenuMigrator, 30, 31) .with(PreferredLanguageMigrator, 31, 32) - .with(AppIdMigrator, 32, CURRENT_VERSION); + .with(AppIdMigrator, 32, 33) + .with(DomainSettingsMigrator, 33, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.spec.ts b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.spec.ts new file mode 100644 index 0000000000..7498769194 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.spec.ts @@ -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; + 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()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts new file mode 100644 index 0000000000..1c5681f2aa --- /dev/null +++ b/libs/common/src/state-migrations/migrations/34-move-domain-settings-to-state-providers.ts @@ -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 { + let updateAccount = false; + + // global state ("neverDomains") + const globalState = await helper.get("global"); + + if (globalState?.neverDomains != null) { + await helper.setToGlobal(neverDomainsDefinition, globalState.neverDomains); + + // delete `neverDomains` from state global + delete globalState.neverDomains; + + await helper.set("global", globalState); + } + + // account state ("defaultUriMatch" and "settings.equivalentDomains") + const accounts = await helper.getAccounts(); + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + + // migrate account state + async function migrateAccount(userId: string, account: ExpectedAccountState): Promise { + 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 { + let updateAccount = false; + + // global state ("neverDomains") + const globalState = (await helper.get("global")) || {}; + const neverDomains: { [key: string]: null } = + await helper.getFromGlobal(neverDomainsDefinition); + + if (neverDomains != null) { + await helper.set("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(); + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + + // rollback account state + async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise { + 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, + }); + } + } + } +} diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index ecad46a411..30b518612d 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -1,5 +1,5 @@ +import { UriMatchStrategySetting } from "../../models/domain/domain-service"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; -import { UriMatchType } from "../enums"; import { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; import { Cipher } from "../models/domain/cipher"; @@ -24,7 +24,7 @@ export abstract class CipherService { getAllDecryptedForUrl: ( url: string, includeOtherTypes?: CipherType[], - defaultMatch?: UriMatchType, + defaultMatch?: UriMatchStrategySetting, ) => Promise; getAllFromApiForOrganization: (organizationId: string) => Promise; /** diff --git a/libs/common/src/vault/enums/index.ts b/libs/common/src/vault/enums/index.ts index e7715f6c04..d7d1d06d2b 100644 --- a/libs/common/src/vault/enums/index.ts +++ b/libs/common/src/vault/enums/index.ts @@ -3,4 +3,3 @@ export * from "./cipher-type"; export * from "./field-type.enum"; export * from "./linked-id-type.enum"; export * from "./secure-note-type.enum"; -export * from "./uri-match-type.enum"; diff --git a/libs/common/src/vault/enums/uri-match-type.enum.ts b/libs/common/src/vault/enums/uri-match-type.enum.ts deleted file mode 100644 index 4a4193ba46..0000000000 --- a/libs/common/src/vault/enums/uri-match-type.enum.ts +++ /dev/null @@ -1,8 +0,0 @@ -export enum UriMatchType { - Domain = 0, - Host = 1, - StartsWith = 2, - Exact = 3, - RegularExpression = 4, - Never = 5, -} diff --git a/libs/common/src/vault/models/api/login-uri.api.ts b/libs/common/src/vault/models/api/login-uri.api.ts index ace8a31700..853f181654 100644 --- a/libs/common/src/vault/models/api/login-uri.api.ts +++ b/libs/common/src/vault/models/api/login-uri.api.ts @@ -1,10 +1,10 @@ +import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { BaseResponse } from "../../../models/response/base.response"; -import { UriMatchType } from "../../enums"; export class LoginUriApi extends BaseResponse { uri: string; uriChecksum: string; - match: UriMatchType = null; + match: UriMatchStrategySetting = null; constructor(data: any = null) { super(data); diff --git a/libs/common/src/vault/models/data/login-uri.data.ts b/libs/common/src/vault/models/data/login-uri.data.ts index 973470ffc7..99d2f9a9eb 100644 --- a/libs/common/src/vault/models/data/login-uri.data.ts +++ b/libs/common/src/vault/models/data/login-uri.data.ts @@ -1,10 +1,10 @@ -import { UriMatchType } from "../../enums"; +import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { LoginUriApi } from "../api/login-uri.api"; export class LoginUriData { uri: string; uriChecksum: string; - match: UriMatchType = null; + match: UriMatchStrategySetting = null; constructor(data?: LoginUriApi) { if (data == null) { diff --git a/libs/common/src/vault/models/domain/cipher.spec.ts b/libs/common/src/vault/models/domain/cipher.spec.ts index 856d6d3887..a75d645831 100644 --- a/libs/common/src/vault/models/domain/cipher.spec.ts +++ b/libs/common/src/vault/models/domain/cipher.spec.ts @@ -2,13 +2,14 @@ import { mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; import { makeStaticByteArray, mockEnc, mockFromJson } from "../../../../spec/utils"; +import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; import { ContainerService } from "../../../platform/services/container.service"; import { InitializerKey } from "../../../platform/services/cryptography/initializer-key"; 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 { CipherType } from "../../enums/cipher-type"; import { CipherData } from "../../models/data/cipher.data"; @@ -76,7 +77,11 @@ describe("Cipher DTO", () => { key: "EncryptedString", login: { uris: [ - { uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain }, + { + uri: "EncryptedString", + uriChecksum: "EncryptedString", + match: UriMatchStrategy.Domain, + }, ], username: "EncryptedString", password: "EncryptedString", diff --git a/libs/common/src/vault/models/domain/login-uri.spec.ts b/libs/common/src/vault/models/domain/login-uri.spec.ts index 1c432cf8d7..c42b0cc9d1 100644 --- a/libs/common/src/vault/models/domain/login-uri.spec.ts +++ b/libs/common/src/vault/models/domain/login-uri.spec.ts @@ -2,9 +2,9 @@ import { MockProxy, mock } from "jest-mock-extended"; import { Jsonify } from "type-fest"; import { mockEnc, mockFromJson } from "../../../../spec"; +import { UriMatchStrategy } from "../../../models/domain/domain-service"; import { EncryptService } from "../../../platform/abstractions/encrypt.service"; import { EncString } from "../../../platform/models/domain/enc-string"; -import { UriMatchType } from "../../enums"; import { LoginUriData } from "../data/login-uri.data"; import { LoginUri } from "./login-uri"; @@ -16,7 +16,7 @@ describe("LoginUri", () => { data = { uri: "encUri", uriChecksum: "encUriChecksum", - match: UriMatchType.Domain, + match: UriMatchStrategy.Domain, }; }); @@ -48,7 +48,7 @@ describe("LoginUri", () => { it("Decrypt", async () => { const loginUri = new LoginUri(); - loginUri.match = UriMatchType.Exact; + loginUri.match = UriMatchStrategy.Exact; loginUri.uri = mockEnc("uri"); const view = await loginUri.decrypt(null); @@ -103,13 +103,13 @@ describe("LoginUri", () => { const actual = LoginUri.fromJSON({ uri: "myUri", uriChecksum: "myUriChecksum", - match: UriMatchType.Domain, + match: UriMatchStrategy.Domain, } as Jsonify); expect(actual).toEqual({ uri: "myUri_fromJSON", uriChecksum: "myUriChecksum_fromJSON", - match: UriMatchType.Domain, + match: UriMatchStrategy.Domain, }); expect(actual).toBeInstanceOf(LoginUri); }); diff --git a/libs/common/src/vault/models/domain/login-uri.ts b/libs/common/src/vault/models/domain/login-uri.ts index dcdd1de294..0a0c5765a3 100644 --- a/libs/common/src/vault/models/domain/login-uri.ts +++ b/libs/common/src/vault/models/domain/login-uri.ts @@ -1,17 +1,17 @@ import { Jsonify } from "type-fest"; +import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; import Domain from "../../../platform/models/domain/domain-base"; import { EncString } from "../../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../../platform/models/domain/symmetric-crypto-key"; -import { UriMatchType } from "../../enums"; import { LoginUriData } from "../data/login-uri.data"; import { LoginUriView } from "../view/login-uri.view"; export class LoginUri extends Domain { uri: EncString; uriChecksum: EncString | undefined; - match: UriMatchType; + match: UriMatchStrategySetting; constructor(obj?: LoginUriData) { super(); diff --git a/libs/common/src/vault/models/domain/login.spec.ts b/libs/common/src/vault/models/domain/login.spec.ts index 232ca035aa..e420a953e6 100644 --- a/libs/common/src/vault/models/domain/login.spec.ts +++ b/libs/common/src/vault/models/domain/login.spec.ts @@ -1,8 +1,8 @@ import { MockProxy, mock } from "jest-mock-extended"; import { mockEnc, mockFromJson } from "../../../../spec"; +import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { EncryptedString, EncString } from "../../../platform/models/domain/enc-string"; -import { UriMatchType } from "../../enums"; import { LoginData } from "../../models/data/login.data"; import { Login } from "../../models/domain/login"; import { LoginUri } from "../../models/domain/login-uri"; @@ -30,7 +30,7 @@ describe("Login DTO", () => { it("Convert from full LoginData", () => { const fido2CredentialData = initializeFido2Credential(new Fido2CredentialData()); const data: LoginData = { - uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }], + uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }], username: "username", password: "password", passwordRevisionDate: "2022-01-31T12:00:00.000Z", @@ -82,7 +82,7 @@ describe("Login DTO", () => { totp: "encrypted totp", uris: [ { - match: null as UriMatchType, + match: null as UriMatchStrategySetting, _uri: "decrypted uri", _domain: null as string, _hostname: null as string, @@ -123,7 +123,7 @@ describe("Login DTO", () => { it("Converts from LoginData and back", () => { const data: LoginData = { - uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchType.Domain }], + uris: [{ uri: "uri", uriChecksum: "checksum", match: UriMatchStrategy.Domain }], username: "username", password: "password", passwordRevisionDate: "2022-01-31T12:00:00.000Z", diff --git a/libs/common/src/vault/models/view/login-uri-view.spec.ts b/libs/common/src/vault/models/view/login-uri-view.spec.ts index 2a806bb685..efc7509629 100644 --- a/libs/common/src/vault/models/view/login-uri-view.spec.ts +++ b/libs/common/src/vault/models/view/login-uri-view.spec.ts @@ -1,26 +1,26 @@ +import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; -import { UriMatchType } from "../../enums"; import { LoginUriView } from "./login-uri.view"; const testData = [ { - match: UriMatchType.Host, + match: UriMatchStrategy.Host, uri: "http://example.com/login", expected: "http://example.com/login", }, { - match: UriMatchType.Host, + match: UriMatchStrategy.Host, uri: "bitwarden.com", expected: "http://bitwarden.com", }, { - match: UriMatchType.Host, + match: UriMatchStrategy.Host, uri: "bitwarden.de", expected: "http://bitwarden.de", }, { - match: UriMatchType.Host, + match: UriMatchStrategy.Host, uri: "bitwarden.br", expected: "http://bitwarden.br", }, @@ -41,7 +41,7 @@ const exampleUris = { describe("LoginUriView", () => { it("isWebsite() given an invalid domain should return false", async () => { 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); }); @@ -67,32 +67,32 @@ describe("LoginUriView", () => { it(`canLaunch should return false when MatchDetection is set to Regex`, async () => { 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); }); it(`canLaunch() should return false when the given protocol does not match CanLaunchWhiteList`, async () => { 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); }); describe("uri matching", () => { describe("using domain matching", () => { 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()); expect(actual).toBe(true); }); 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()); expect(actual).toBe(true); }); 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( exampleUris.differentDomain, exampleUris.noEquivalentDomains(), @@ -103,7 +103,7 @@ describe("LoginUriView", () => { // Actual integration test with the real blacklist, not ideal it("does not match domains that are blacklisted", () => { 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); @@ -113,13 +113,13 @@ describe("LoginUriView", () => { describe("using host matching", () => { 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()); expect(actual).toBe(true); }); 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()); expect(actual).toBe(false); }); @@ -127,13 +127,13 @@ describe("LoginUriView", () => { describe("using exact matching", () => { 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()); expect(actual).toBe(true); }); 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( exampleUris.standard + "#", exampleUris.noEquivalentDomains(), @@ -144,7 +144,7 @@ describe("LoginUriView", () => { describe("using startsWith matching", () => { 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( exampleUris.standard + "#bookmark", exampleUris.noEquivalentDomains(), @@ -153,7 +153,7 @@ describe("LoginUriView", () => { }); 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( exampleUris.standard.slice(1), exampleUris.noEquivalentDomains(), @@ -164,13 +164,13 @@ describe("LoginUriView", () => { describe("using regular expression matching", () => { 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()); expect(actual).toBe(false); }); 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()); expect(actual).toBe(false); }); @@ -178,7 +178,7 @@ describe("LoginUriView", () => { describe("using never matching", () => { 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()); 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(); loginUri.match = match; loginUri.uri = uri; diff --git a/libs/common/src/vault/models/view/login-uri.view.ts b/libs/common/src/vault/models/view/login-uri.view.ts index 79b0432ec8..f3bc0a492d 100644 --- a/libs/common/src/vault/models/view/login-uri.view.ts +++ b/libs/common/src/vault/models/view/login-uri.view.ts @@ -1,13 +1,13 @@ import { Jsonify } from "type-fest"; +import { UriMatchStrategy, UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { View } from "../../../models/view/view"; import { SafeUrls } from "../../../platform/misc/safe-urls"; import { Utils } from "../../../platform/misc/utils"; -import { UriMatchType } from "../../enums"; import { LoginUri } from "../domain/login-uri"; export class LoginUriView implements View { - match: UriMatchType = null; + match: UriMatchStrategySetting = null; private _uri: string = null; private _domain: string = null; @@ -44,7 +44,7 @@ export class LoginUriView implements View { } get hostname(): string { - if (this.match === UriMatchType.RegularExpression) { + if (this.match === UriMatchStrategy.RegularExpression) { return null; } if (this._hostname == null && this.uri != null) { @@ -58,7 +58,7 @@ export class LoginUriView implements View { } get host(): string { - if (this.match === UriMatchType.RegularExpression) { + if (this.match === UriMatchStrategy.RegularExpression) { return null; } if (this._host == null && this.uri != null) { @@ -92,7 +92,7 @@ export class LoginUriView implements View { if (this._canLaunch != null) { 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); } else { this._canLaunch = false; @@ -113,30 +113,30 @@ export class LoginUriView implements View { matchesUri( targetUri: string, equivalentDomains: Set, - defaultUriMatch: UriMatchType = null, + defaultUriMatch: UriMatchStrategySetting = null, ): boolean { if (!this.uri || !targetUri) { return false; } let matchType = this.match ?? defaultUriMatch; - matchType ??= UriMatchType.Domain; + matchType ??= UriMatchStrategy.Domain; const targetDomain = Utils.getDomain(targetUri); const matchDomains = equivalentDomains.add(targetDomain); switch (matchType) { - case UriMatchType.Domain: + case UriMatchStrategy.Domain: return this.matchesDomain(targetUri, matchDomains); - case UriMatchType.Host: { + case UriMatchStrategy.Host: { const urlHost = Utils.getHost(targetUri); return urlHost != null && urlHost === Utils.getHost(this.uri); } - case UriMatchType.Exact: + case UriMatchStrategy.Exact: return targetUri === this.uri; - case UriMatchType.StartsWith: + case UriMatchStrategy.StartsWith: return targetUri.startsWith(this.uri); - case UriMatchType.RegularExpression: + case UriMatchStrategy.RegularExpression: try { const regex = new RegExp(this.uri, "i"); return regex.test(targetUri); @@ -144,7 +144,7 @@ export class LoginUriView implements View { // Invalid regex return false; } - case UriMatchType.Never: + case UriMatchStrategy.Never: return false; default: break; diff --git a/libs/common/src/vault/models/view/login.view.ts b/libs/common/src/vault/models/view/login.view.ts index 59b21b901d..53bbc22022 100644 --- a/libs/common/src/vault/models/view/login.view.ts +++ b/libs/common/src/vault/models/view/login.view.ts @@ -1,6 +1,7 @@ +import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { Utils } from "../../../platform/misc/utils"; 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 { Login } from "../domain/login"; @@ -71,7 +72,7 @@ export class LoginView extends ItemView { matchesUri( targetUri: string, equivalentDomains: Set, - defaultUriMatch: UriMatchType = null, + defaultUriMatch: UriMatchStrategySetting = null, ): boolean { if (this.uris == null) { return false; diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 80fe1d7f3b..bcd4bb9836 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -4,8 +4,9 @@ import { of } from "rxjs"; import { makeStaticByteArray } from "../../../spec/utils"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; -import { SettingsService } from "../../abstractions/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 { CryptoService } from "../../platform/abstractions/crypto.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 { CipherKey, OrgKey } from "../../types/key"; 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 { CipherType } from "../enums/cipher-type"; import { CipherData } from "../models/data/cipher.data"; @@ -53,7 +54,9 @@ const cipherData: CipherData = { key: "EncKey", reprompt: CipherRepromptType.None, login: { - uris: [{ uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchType.Domain }], + uris: [ + { uri: "EncryptedString", uriChecksum: "EncryptedString", match: UriMatchStrategy.Domain }, + ], username: "EncryptedString", password: "EncryptedString", passwordRevisionDate: "2022-01-31T12:00:00.000Z", @@ -99,7 +102,7 @@ describe("Cipher Service", () => { const cryptoService = mock(); const stateService = mock(); const autofillSettingsService = mock(); - const settingsService = mock(); + const domainSettingsService = mock(); const apiService = mock(); const cipherFileUploadService = mock(); const i18nService = mock(); @@ -118,7 +121,7 @@ describe("Cipher Service", () => { cipherService = new CipherService( cryptoService, - settingsService, + domainSettingsService, apiService, i18nService, searchService, @@ -277,7 +280,7 @@ describe("Cipher Service", () => { it("should add a uri hash to login uris", async () => { encryptService.hash.mockImplementation((value) => Promise.resolve(`${value} hash`)); cipherView.login.uris = [ - { uri: "uri", match: UriMatchType.RegularExpression } as LoginUriView, + { uri: "uri", match: UriMatchStrategy.RegularExpression } as LoginUriView, ]; const domain = await cipherService.encrypt(cipherView); @@ -286,7 +289,7 @@ describe("Cipher Service", () => { { uri: new EncString("uri has been encrypted"), uriChecksum: new EncString("uri hash has been encrypted"), - match: UriMatchType.RegularExpression, + match: UriMatchStrategy.RegularExpression, }, ]); }); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index f5256342aa..b3deed7c0c 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -3,8 +3,9 @@ import { SemVer } from "semver"; import { ApiService } from "../../abstractions/api.service"; import { SearchService } from "../../abstractions/search.service"; -import { SettingsService } from "../../abstractions/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 { ListResponse } from "../../models/response/list.response"; 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 { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.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 { CipherData } from "../models/data/cipher.data"; import { Attachment } from "../models/domain/attachment"; @@ -61,7 +62,7 @@ export class CipherService implements CipherServiceAbstraction { constructor( private cryptoService: CryptoService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private apiService: ApiService, private i18nService: I18nService, private searchService: SearchService, @@ -355,15 +356,17 @@ export class CipherService implements CipherServiceAbstraction { async getAllDecryptedForUrl( url: string, includeOtherTypes?: CipherType[], - defaultMatch: UriMatchType = null, + defaultMatch: UriMatchStrategySetting = null, ): Promise { if (url == null && includeOtherTypes == null) { return Promise.resolve([]); } - const equivalentDomains = this.settingsService.getEquivalentDomains(url); + const equivalentDomains = await firstValueFrom( + this.domainSettingsService.getUrlEquivalentDomains(url), + ); const ciphers = await this.getAllDecrypted(); - defaultMatch ??= await this.stateService.getDefaultUriMatch(); + defaultMatch ??= await firstValueFrom(this.domainSettingsService.defaultUriMatchStrategy$); return ciphers.filter((cipher) => { const cipherIsLogin = cipher.type === CipherType.Login && cipher.login !== null; @@ -503,12 +506,12 @@ export class CipherService implements CipherServiceAbstraction { return; } - let domains = await this.stateService.getNeverDomains(); + let domains = await firstValueFrom(this.domainSettingsService.neverDomains$); if (!domains) { domains = {}; } domains[domain] = null; - await this.stateService.setNeverDomains(domains); + await this.domainSettingsService.setNeverDomains(domains); } async createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise { diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts index 46d5e2d9c8..dd90c53cb1 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.spec.ts @@ -3,6 +3,7 @@ import { of } from "rxjs"; import { AuthService } from "../../../auth/abstractions/auth.service"; 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 { StateService } from "../../../platform/abstractions/state.service"; import { Utils } from "../../../platform/misc/utils"; @@ -34,6 +35,7 @@ describe("FidoAuthenticatorService", () => { let authService!: MockProxy; let stateService!: MockProxy; let vaultSettingsService: MockProxy; + let domainSettingsService: MockProxy; let client!: Fido2ClientService; let tab!: chrome.tabs.Tab; @@ -43,6 +45,7 @@ describe("FidoAuthenticatorService", () => { authService = mock(); stateService = mock(); vaultSettingsService = mock(); + domainSettingsService = mock(); client = new Fido2ClientService( authenticator, @@ -50,9 +53,11 @@ describe("FidoAuthenticatorService", () => { authService, stateService, vaultSettingsService, + domainSettingsService, ); configService.serverConfig$ = of({ environment: { vault: VaultUrl } } as any); vaultSettingsService.enablePasskeys$ = of(true); + domainSettingsService.neverDomains$ = of({}); authService.getAuthStatus.mockResolvedValue(AuthenticationStatus.Unlocked); tab = { id: 123, windowId: 456 } as chrome.tabs.Tab; }); @@ -130,7 +135,7 @@ describe("FidoAuthenticatorService", () => { origin: "https://bitwarden.com", 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); @@ -376,7 +381,8 @@ describe("FidoAuthenticatorService", () => { const params = createParams({ origin: "https://bitwarden.com", }); - stateService.getNeverDomains.mockResolvedValue({ "bitwarden.com": null }); + + domainSettingsService.neverDomains$ = of({ "bitwarden.com": null }); const result = async () => await client.assertCredential(params, tab); diff --git a/libs/common/src/vault/services/fido2/fido2-client.service.ts b/libs/common/src/vault/services/fido2/fido2-client.service.ts index 764973b956..257700453e 100644 --- a/libs/common/src/vault/services/fido2/fido2-client.service.ts +++ b/libs/common/src/vault/services/fido2/fido2-client.service.ts @@ -3,6 +3,7 @@ import { parse } from "tldts"; import { AuthService } from "../../../auth/abstractions/auth.service"; 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 { LogService } from "../../../platform/abstractions/log.service"; import { StateService } from "../../../platform/abstractions/state.service"; @@ -44,6 +45,7 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { private authService: AuthService, private stateService: StateService, private vaultSettingsService: VaultSettingsService, + private domainSettingsService: DomainSettingsService, private logService?: LogService, ) {} @@ -52,7 +54,8 @@ export class Fido2ClientService implements Fido2ClientServiceAbstraction { const isUserLoggedIn = (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 serverConfig = await firstValueFrom(this.configService.serverConfig$); diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index c0105af758..200acf97f1 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -1,5 +1,4 @@ import { ApiService } from "../../../abstractions/api.service"; -import { SettingsService } from "../../../abstractions/settings.service"; import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; 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 { KeyConnectorService } from "../../../auth/abstractions/key-connector.service"; 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 { SyncCipherNotification, @@ -44,7 +44,7 @@ export class SyncService implements SyncServiceAbstraction { constructor( private apiService: ApiService, - private settingsService: SettingsService, + private domainSettingsService: DomainSettingsService, private folderService: InternalFolderService, private cipherService: CipherService, 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[]) {