mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-25 12:15:18 +01:00
[PM-5562] Implement Domain Settings state provider (#8226)
* create domain settings state provider * replace callsites for defaultUriMatch and neverDomains with DomainSettingsService equivalents * replace callsites for equivalentDomains with DomainSettingsService equivalents and clean up unused AccountSettingsSettings * add migrations for domain settings state * do not use enum for URI match strategy constants and types * add getUrlEquivalentDomains test * PR suggestions/cleanup * refactor getUrlEquivalentDomains to return an observable Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> * update tests * add UriMatchStrategy docs notes * service class renames * use service abstraction at callsites previously using service class directly --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com>
This commit is contained in:
parent
a0e0637bb6
commit
0a595ea95e
@ -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<void>;
|
||||
bgGetEnableChangedPasswordPrompt: () => Promise<boolean>;
|
||||
bgGetEnableAddedLoginPrompt: () => Promise<boolean>;
|
||||
bgGetExcludedDomains: () => Promise<NeverDomains>;
|
||||
getWebVaultUrlForNotification: () => string;
|
||||
};
|
||||
|
||||
|
@ -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<FolderService>();
|
||||
const stateService = mock<BrowserStateService>();
|
||||
const userNotificationSettingsService = mock<UserNotificationSettingsService>();
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const environmentService = mock<EnvironmentService>();
|
||||
const logService = mock<LogService>();
|
||||
|
||||
@ -59,6 +61,7 @@ describe("NotificationBackground", () => {
|
||||
folderService,
|
||||
stateService,
|
||||
userNotificationSettingsService,
|
||||
domainSettingsService,
|
||||
environmentService,
|
||||
logService,
|
||||
);
|
||||
|
@ -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<NeverDomains> {
|
||||
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.
|
||||
|
@ -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),
|
||||
),
|
||||
);
|
||||
|
@ -0,0 +1,25 @@
|
||||
import { DefaultDomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
|
||||
|
||||
import {
|
||||
CachedServices,
|
||||
factory,
|
||||
FactoryOptions,
|
||||
} from "../../../platform/background/service-factories/factory-options";
|
||||
import {
|
||||
stateProviderFactory,
|
||||
StateProviderInitOptions,
|
||||
} from "../../../platform/background/service-factories/state-provider.factory";
|
||||
|
||||
export type DomainSettingsServiceInitOptions = FactoryOptions & StateProviderInitOptions;
|
||||
|
||||
export function domainSettingsServiceFactory(
|
||||
cache: { domainSettingsService?: DefaultDomainSettingsService } & CachedServices,
|
||||
opts: DomainSettingsServiceInitOptions,
|
||||
): Promise<DefaultDomainSettingsService> {
|
||||
return factory(
|
||||
cache,
|
||||
"domainSettingsService",
|
||||
opts,
|
||||
async () => new DefaultDomainSettingsService(await stateProviderFactory(cache, opts)),
|
||||
);
|
||||
}
|
@ -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();
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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<CipherService>();
|
||||
const stateService = mock<BrowserStateService>();
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const mockUserId = Utils.newGuid() as UserId;
|
||||
const accountService: FakeAccountService = mockAccountServiceWith(mockUserId);
|
||||
const fakeStateProvider: FakeStateProvider = new FakeStateProvider(accountService);
|
||||
let domainSettingsService: DomainSettingsService;
|
||||
const totpService = mock<TotpService>();
|
||||
const eventCollectionService = mock<EventCollectionService>();
|
||||
const logService = mock<LogService>();
|
||||
const settingsService = mock<SettingsService>();
|
||||
const userVerificationService = mock<UserVerificationService>();
|
||||
|
||||
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<LoginUriView>({
|
||||
uri: "https://www.example.com",
|
||||
match: UriMatchType.Domain,
|
||||
match: UriMatchStrategy.Domain,
|
||||
});
|
||||
options = createGenerateFillScriptOptionsMock();
|
||||
options.cipher.login = mock<LoginView>({
|
||||
@ -1559,13 +1578,13 @@ describe("AutofillService", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("skips adding any login uri views that have a UriMatchType of Never to the list of saved urls", async () => {
|
||||
it("skips adding any login uri views that have a UriMatchStrategySetting of Never to the list of saved urls", async () => {
|
||||
const secondUriView = mock<LoginUriView>({
|
||||
uri: "https://www.second-example.com",
|
||||
});
|
||||
const thirdUriView = mock<LoginUriView>({
|
||||
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,
|
||||
|
@ -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<UriMatchStrategySetting> {
|
||||
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<boolean> {
|
||||
// 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,
|
||||
|
@ -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<CipherView>(),
|
||||
tabUrl: "https://jest-testing-website.com",
|
||||
defaultUriMatch: UriMatchType.Domain,
|
||||
defaultUriMatch: UriMatchStrategy.Domain,
|
||||
...customFields,
|
||||
};
|
||||
}
|
||||
|
@ -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<GlobalState, "neverDomains">;
|
||||
|
||||
/**
|
||||
* A HTMLElement (usually a form element) with additional custom properties added by this script
|
||||
*/
|
||||
|
@ -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),
|
||||
|
@ -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"]);
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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<AccountSettingsSettings>;
|
||||
|
||||
@sessionSync({ initializer: (b: boolean) => b })
|
||||
protected _disableFavicon: BehaviorSubject<boolean>;
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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),
|
||||
|
@ -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);
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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 },
|
||||
|
@ -1,14 +1,8 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { AccountSettingsSettings } from "../platform/models/domain/account";
|
||||
|
||||
export abstract class SettingsService {
|
||||
settings$: Observable<AccountSettingsSettings>;
|
||||
disableFavicon$: Observable<boolean>;
|
||||
|
||||
setEquivalentDomains: (equivalentDomains: string[][]) => Promise<any>;
|
||||
getEquivalentDomains: (url: string) => Set<string>;
|
||||
setDisableFavicon: (value: boolean) => Promise<any>;
|
||||
getDisableFavicon: () => boolean;
|
||||
clear: (userId?: string) => Promise<void>;
|
||||
}
|
||||
|
@ -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());
|
||||
});
|
||||
});
|
||||
});
|
97
libs/common/src/autofill/services/domain-settings.service.ts
Normal file
97
libs/common/src/autofill/services/domain-settings.service.ts
Normal file
@ -0,0 +1,97 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import {
|
||||
NeverDomains,
|
||||
EquivalentDomains,
|
||||
UriMatchStrategySetting,
|
||||
UriMatchStrategy,
|
||||
} from "../../models/domain/domain-service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import {
|
||||
DOMAIN_SETTINGS_DISK,
|
||||
ActiveUserState,
|
||||
GlobalState,
|
||||
KeyDefinition,
|
||||
StateProvider,
|
||||
UserKeyDefinition,
|
||||
} from "../../platform/state";
|
||||
|
||||
const NEVER_DOMAINS = new KeyDefinition(DOMAIN_SETTINGS_DISK, "neverDomains", {
|
||||
deserializer: (value: NeverDomains) => value ?? null,
|
||||
});
|
||||
|
||||
const EQUIVALENT_DOMAINS = new UserKeyDefinition(DOMAIN_SETTINGS_DISK, "equivalentDomains", {
|
||||
deserializer: (value: EquivalentDomains) => value ?? null,
|
||||
clearOn: ["logout"],
|
||||
});
|
||||
|
||||
const DEFAULT_URI_MATCH_STRATEGY = new KeyDefinition(
|
||||
DOMAIN_SETTINGS_DISK,
|
||||
"defaultUriMatchStrategy",
|
||||
{
|
||||
deserializer: (value: UriMatchStrategySetting) => value ?? UriMatchStrategy.Domain,
|
||||
},
|
||||
);
|
||||
|
||||
export abstract class DomainSettingsService {
|
||||
neverDomains$: Observable<NeverDomains>;
|
||||
setNeverDomains: (newValue: NeverDomains) => Promise<void>;
|
||||
equivalentDomains$: Observable<EquivalentDomains>;
|
||||
setEquivalentDomains: (newValue: EquivalentDomains) => Promise<void>;
|
||||
defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||
setDefaultUriMatchStrategy: (newValue: UriMatchStrategySetting) => Promise<void>;
|
||||
getUrlEquivalentDomains: (url: string) => Observable<Set<string>>;
|
||||
}
|
||||
|
||||
export class DefaultDomainSettingsService implements DomainSettingsService {
|
||||
private neverDomainsState: GlobalState<NeverDomains>;
|
||||
readonly neverDomains$: Observable<NeverDomains>;
|
||||
|
||||
private equivalentDomainsState: ActiveUserState<EquivalentDomains>;
|
||||
readonly equivalentDomains$: Observable<EquivalentDomains>;
|
||||
|
||||
private defaultUriMatchStrategyState: ActiveUserState<UriMatchStrategySetting>;
|
||||
readonly defaultUriMatchStrategy$: Observable<UriMatchStrategySetting>;
|
||||
|
||||
constructor(private stateProvider: StateProvider) {
|
||||
this.neverDomainsState = this.stateProvider.getGlobal(NEVER_DOMAINS);
|
||||
this.neverDomains$ = this.neverDomainsState.state$.pipe(map((x) => x ?? null));
|
||||
|
||||
this.equivalentDomainsState = this.stateProvider.getActive(EQUIVALENT_DOMAINS);
|
||||
this.equivalentDomains$ = this.equivalentDomainsState.state$.pipe(map((x) => x ?? null));
|
||||
|
||||
this.defaultUriMatchStrategyState = this.stateProvider.getActive(DEFAULT_URI_MATCH_STRATEGY);
|
||||
this.defaultUriMatchStrategy$ = this.defaultUriMatchStrategyState.state$.pipe(
|
||||
map((x) => x ?? UriMatchStrategy.Domain),
|
||||
);
|
||||
}
|
||||
|
||||
async setNeverDomains(newValue: NeverDomains): Promise<void> {
|
||||
await this.neverDomainsState.update(() => newValue);
|
||||
}
|
||||
|
||||
async setEquivalentDomains(newValue: EquivalentDomains): Promise<void> {
|
||||
await this.equivalentDomainsState.update(() => newValue);
|
||||
}
|
||||
|
||||
async setDefaultUriMatchStrategy(newValue: UriMatchStrategySetting): Promise<void> {
|
||||
await this.defaultUriMatchStrategyState.update(() => newValue);
|
||||
}
|
||||
|
||||
getUrlEquivalentDomains(url: string): Observable<Set<string>> {
|
||||
const domains$ = this.equivalentDomains$.pipe(
|
||||
map((equivalentDomains) => {
|
||||
const domain = Utils.getDomain(url);
|
||||
if (domain == null || equivalentDomains == null) {
|
||||
return new Set() as Set<string>;
|
||||
}
|
||||
|
||||
const equivalents = equivalentDomains.filter((ed) => ed.includes(domain)).flat();
|
||||
|
||||
return new Set(equivalents);
|
||||
}),
|
||||
);
|
||||
|
||||
return domains$;
|
||||
}
|
||||
}
|
24
libs/common/src/models/domain/domain-service.ts
Normal file
24
libs/common/src/models/domain/domain-service.ts
Normal file
@ -0,0 +1,24 @@
|
||||
/*
|
||||
See full documentation at:
|
||||
https://bitwarden.com/help/uri-match-detection/#match-detection-options
|
||||
|
||||
Domain: "the top-level domain and second-level domain of the URI match the detected resource",
|
||||
Host: "the hostname and (if specified) port of the URI matches the detected resource",
|
||||
StartsWith: "the detected resource starts with the URI, regardless of what follows it",
|
||||
Exact: "the URI matches the detected resource exactly",
|
||||
RegularExpression: "the detected resource matches a specified regular expression",
|
||||
Never: "never offer auto-fill for the item",
|
||||
*/
|
||||
export const UriMatchStrategy = {
|
||||
Domain: 0,
|
||||
Host: 1,
|
||||
StartsWith: 2,
|
||||
Exact: 3,
|
||||
RegularExpression: 4,
|
||||
Never: 5,
|
||||
} as const;
|
||||
|
||||
export type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||
|
||||
export type NeverDomains = { [id: string]: unknown };
|
||||
export type EquivalentDomains = string[][];
|
@ -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) {
|
||||
|
@ -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<T extends Account = Account> {
|
||||
* @deprecated Do not call this directly, use SendService
|
||||
*/
|
||||
setDecryptedSends: (value: SendView[], options?: StorageOptions) => Promise<void>;
|
||||
getDefaultUriMatch: (options?: StorageOptions) => Promise<UriMatchType>;
|
||||
setDefaultUriMatch: (value: UriMatchType, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this, use SettingsService
|
||||
*/
|
||||
@ -275,8 +268,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
* @deprecated Do not call this directly, use SendService
|
||||
*/
|
||||
setEncryptedSends: (value: { [id: string]: SendData }, options?: StorageOptions) => Promise<void>;
|
||||
getEquivalentDomains: (options?: StorageOptions) => Promise<string[][]>;
|
||||
setEquivalentDomains: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getEventCollection: (options?: StorageOptions) => Promise<EventData[]>;
|
||||
setEventCollection: (value: EventData[], options?: StorageOptions) => Promise<void>;
|
||||
getEverBeenUnlocked: (options?: StorageOptions) => Promise<boolean>;
|
||||
@ -310,8 +301,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setMainWindowSize: (value: number, options?: StorageOptions) => Promise<void>;
|
||||
getMinimizeOnCopyToClipboard: (options?: StorageOptions) => Promise<boolean>;
|
||||
setMinimizeOnCopyToClipboard: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getNeverDomains: (options?: StorageOptions) => Promise<{ [id: string]: unknown }>;
|
||||
setNeverDomains: (value: { [id: string]: unknown }, options?: StorageOptions) => Promise<void>;
|
||||
getOpenAtLogin: (options?: StorageOptions) => Promise<boolean>;
|
||||
setOpenAtLogin: (value: boolean, options?: StorageOptions) => Promise<void>;
|
||||
getOrganizationInvitation: (options?: StorageOptions) => Promise<any>;
|
||||
@ -353,14 +342,6 @@ export abstract class StateService<T extends Account = Account> {
|
||||
setRememberedEmail: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
getSecurityStamp: (options?: StorageOptions) => Promise<string>;
|
||||
setSecurityStamp: (value: string, options?: StorageOptions) => Promise<void>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use SettingsService
|
||||
*/
|
||||
getSettings: (options?: StorageOptions) => Promise<AccountSettingsSettings>;
|
||||
/**
|
||||
* @deprecated Do not call this directly, use SettingsService
|
||||
*/
|
||||
setSettings: (value: AccountSettingsSettings, options?: StorageOptions) => Promise<void>;
|
||||
getTheme: (options?: StorageOptions) => Promise<ThemeType>;
|
||||
setTheme: (value: ThemeType, options?: StorageOptions) => Promise<void>;
|
||||
getTwoFactorToken: (options?: StorageOptions) => Promise<string>;
|
||||
|
@ -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;
|
||||
|
@ -25,6 +25,5 @@ export class GlobalState {
|
||||
enableBrowserIntegration?: boolean;
|
||||
enableBrowserIntegrationFingerprint?: boolean;
|
||||
enableDuckDuckGoBrowserIntegration?: boolean;
|
||||
neverDomains?: { [id: string]: unknown };
|
||||
deepLinkRedirectUrl?: string;
|
||||
}
|
||||
|
@ -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<UriMatchType> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.settings?.defaultUriMatch;
|
||||
}
|
||||
|
||||
async setDefaultUriMatch(value: UriMatchType, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.settings.defaultUriMatch = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getDisableFavicon(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(
|
||||
@ -1333,23 +1314,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getEquivalentDomains(options?: StorageOptions): Promise<string[][]> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.settings?.equivalentDomains;
|
||||
}
|
||||
|
||||
async setEquivalentDomains(value: string, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
account.settings.equivalentDomains = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
@withPrototypeForArrayMembers(EventData)
|
||||
async getEventCollection(options?: StorageOptions): Promise<EventData[]> {
|
||||
return (
|
||||
@ -1609,23 +1573,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getNeverDomains(options?: StorageOptions): Promise<{ [id: string]: unknown }> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))
|
||||
)?.neverDomains;
|
||||
}
|
||||
|
||||
async setNeverDomains(value: { [id: string]: unknown }, options?: StorageOptions): Promise<void> {
|
||||
const globals = await this.getGlobals(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
globals.neverDomains = value;
|
||||
await this.saveGlobals(
|
||||
globals,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getOpenAtLogin(options?: StorageOptions): Promise<boolean> {
|
||||
return (
|
||||
(await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions())))
|
||||
@ -1807,23 +1754,6 @@ export class StateService<
|
||||
);
|
||||
}
|
||||
|
||||
async getSettings(options?: StorageOptions): Promise<AccountSettingsSettings> {
|
||||
return (
|
||||
await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()))
|
||||
)?.settings?.settings;
|
||||
}
|
||||
|
||||
async setSettings(value: AccountSettingsSettings, options?: StorageOptions): Promise<void> {
|
||||
const account = await this.getAccount(
|
||||
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
|
||||
);
|
||||
account.settings.settings = value;
|
||||
await this.saveAccount(
|
||||
account,
|
||||
this.reconcileOptions(options, await this.defaultOnDiskMemoryOptions()),
|
||||
);
|
||||
}
|
||||
|
||||
async getTheme(options?: StorageOptions): Promise<ThemeType> {
|
||||
return (
|
||||
await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()))
|
||||
|
@ -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",
|
||||
|
@ -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);
|
||||
|
@ -1,83 +0,0 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { CryptoService } from "../platform/abstractions/crypto.service";
|
||||
import { EncryptService } from "../platform/abstractions/encrypt.service";
|
||||
import { StateService } from "../platform/abstractions/state.service";
|
||||
import { ContainerService } from "../platform/services/container.service";
|
||||
|
||||
import { SettingsService } from "./settings.service";
|
||||
|
||||
describe("SettingsService", () => {
|
||||
let settingsService: SettingsService;
|
||||
|
||||
let cryptoService: MockProxy<CryptoService>;
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let stateService: MockProxy<StateService>;
|
||||
let activeAccount: BehaviorSubject<string>;
|
||||
let activeAccountUnlocked: BehaviorSubject<boolean>;
|
||||
|
||||
const mockEquivalentDomains = [
|
||||
["example.com", "exampleapp.com", "example.co.uk", "ejemplo.es"],
|
||||
["bitwarden.com", "bitwarden.co.uk", "sm-bitwarden.com"],
|
||||
["example.co.uk", "exampleapp.co.uk"],
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
cryptoService = mock<CryptoService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
stateService = mock<StateService>();
|
||||
activeAccount = new BehaviorSubject("123");
|
||||
activeAccountUnlocked = new BehaviorSubject(true);
|
||||
|
||||
stateService.getSettings.mockResolvedValue({ equivalentDomains: mockEquivalentDomains });
|
||||
stateService.activeAccount$ = activeAccount;
|
||||
stateService.activeAccountUnlocked$ = activeAccountUnlocked;
|
||||
(window as any).bitwardenContainerService = new ContainerService(cryptoService, encryptService);
|
||||
|
||||
settingsService = new SettingsService(stateService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
activeAccount.complete();
|
||||
activeAccountUnlocked.complete();
|
||||
});
|
||||
|
||||
describe("getEquivalentDomains", () => {
|
||||
it("returns all equivalent domains for a URL", async () => {
|
||||
const actual = settingsService.getEquivalentDomains("example.co.uk");
|
||||
const expected = new Set([
|
||||
"example.com",
|
||||
"exampleapp.com",
|
||||
"example.co.uk",
|
||||
"ejemplo.es",
|
||||
"exampleapp.co.uk",
|
||||
]);
|
||||
expect(actual).toEqual(expected);
|
||||
});
|
||||
|
||||
it("returns an empty set if there are no equivalent domains", () => {
|
||||
const actual = settingsService.getEquivalentDomains("asdf");
|
||||
expect(actual).toEqual(new Set());
|
||||
});
|
||||
});
|
||||
|
||||
it("setEquivalentDomains", async () => {
|
||||
await settingsService.setEquivalentDomains([["test2"], ["domains2"]]);
|
||||
|
||||
expect(stateService.setSettings).toBeCalledTimes(1);
|
||||
|
||||
expect((await firstValueFrom(settingsService.settings$)).equivalentDomains).toEqual([
|
||||
["test2"],
|
||||
["domains2"],
|
||||
]);
|
||||
});
|
||||
|
||||
it("clear", async () => {
|
||||
await settingsService.clear();
|
||||
|
||||
expect(stateService.setSettings).toBeCalledTimes(1);
|
||||
|
||||
expect(await firstValueFrom(settingsService.settings$)).toEqual({});
|
||||
});
|
||||
});
|
@ -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<AccountSettingsSettings> = new BehaviorSubject({});
|
||||
protected _disableFavicon = new BehaviorSubject<boolean>(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<void> {
|
||||
const settings = this._settings.getValue() ?? {};
|
||||
|
||||
settings.equivalentDomains = equivalentDomains;
|
||||
|
||||
this._settings.next(settings);
|
||||
await this.stateService.setSettings(settings);
|
||||
}
|
||||
|
||||
getEquivalentDomains(url: string): Set<string> {
|
||||
const domain = Utils.getDomain(url);
|
||||
if (domain == null) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
const settings = this._settings.getValue();
|
||||
|
||||
let result: string[] = [];
|
||||
|
||||
if (settings?.equivalentDomains != null) {
|
||||
settings.equivalentDomains
|
||||
.filter((ed) => ed.length > 0 && ed.includes(domain))
|
||||
.forEach((ed) => {
|
||||
result = result.concat(ed);
|
||||
});
|
||||
}
|
||||
|
||||
return new Set(result);
|
||||
}
|
||||
|
||||
async setDisableFavicon(value: boolean) {
|
||||
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<void> {
|
||||
if (userId == null || userId == (await this.stateService.getUserId())) {
|
||||
this._settings.next({});
|
||||
}
|
||||
|
||||
await this.stateService.setSettings(null, { userId: userId });
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -0,0 +1,255 @@
|
||||
import { any, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { StateDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { mockMigrationHelper } from "../migration-helper.spec";
|
||||
|
||||
import { DomainSettingsMigrator } from "./34-move-domain-settings-to-state-providers";
|
||||
|
||||
const mockNeverDomains = { "bitwarden.test": null, locahost: null, "www.example.com": null } as {
|
||||
[key: string]: null;
|
||||
};
|
||||
|
||||
function exampleJSON() {
|
||||
return {
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
neverDomains: mockNeverDomains,
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
defaultUriMatch: 3,
|
||||
settings: {
|
||||
equivalentDomains: [] as string[][],
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
settings: {
|
||||
equivalentDomains: [["apple.com", "icloud.com"]],
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
defaultUriMatch: 1,
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
"user-4": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff8",
|
||||
},
|
||||
otherStuff: "otherStuff9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function rollbackJSON() {
|
||||
return {
|
||||
global_domainSettings_neverDomains: mockNeverDomains,
|
||||
"user_user-1_domainSettings_defaultUriMatchStrategy": 3,
|
||||
"user_user-1_domainSettings_equivalentDomains": [] as string[][],
|
||||
"user_user-2_domainSettings_equivalentDomains": [["apple.com", "icloud.com"]],
|
||||
"user_user-3_domainSettings_defaultUriMatchStrategy": 1,
|
||||
global: {
|
||||
otherStuff: "otherStuff1",
|
||||
},
|
||||
authenticatedAccounts: ["user-1", "user-2", "user-3"],
|
||||
"user-1": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
},
|
||||
"user-2": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
},
|
||||
"user-3": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
},
|
||||
"user-4": {
|
||||
settings: {
|
||||
otherStuff: "otherStuff8",
|
||||
},
|
||||
otherStuff: "otherStuff9",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const domainSettingsStateDefinition: {
|
||||
stateDefinition: StateDefinitionLike;
|
||||
} = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
};
|
||||
|
||||
describe("DomainSettingsMigrator", () => {
|
||||
let helper: MockProxy<MigrationHelper>;
|
||||
let sut: DomainSettingsMigrator;
|
||||
|
||||
describe("migrate", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(exampleJSON(), 33);
|
||||
sut = new DomainSettingsMigrator(33, 34);
|
||||
});
|
||||
|
||||
it("should remove global neverDomains and defaultUriMatch and equivalentDomains settings from all accounts", async () => {
|
||||
await sut.migrate(helper);
|
||||
expect(helper.set).toHaveBeenCalledTimes(4);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-3", {
|
||||
settings: {
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
});
|
||||
});
|
||||
|
||||
it("should set global neverDomains and defaultUriMatchStrategy and equivalentDomains setting values for each account", async () => {
|
||||
await sut.migrate(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...domainSettingsStateDefinition, key: "neverDomains" },
|
||||
mockNeverDomains,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
3,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
[],
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
[["apple.com", "icloud.com"]],
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-3",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rollback", () => {
|
||||
beforeEach(() => {
|
||||
helper = mockMigrationHelper(rollbackJSON(), 34);
|
||||
sut = new DomainSettingsMigrator(33, 34);
|
||||
});
|
||||
|
||||
it("should null out new values globally and for each account", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.setToGlobal).toHaveBeenCalledTimes(1);
|
||||
expect(helper.setToGlobal).toHaveBeenCalledWith(
|
||||
{ ...domainSettingsStateDefinition, key: "neverDomains" },
|
||||
null,
|
||||
);
|
||||
|
||||
expect(helper.setToUser).toHaveBeenCalledTimes(4);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-1",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-2",
|
||||
{ ...domainSettingsStateDefinition, key: "equivalentDomains" },
|
||||
null,
|
||||
);
|
||||
expect(helper.setToUser).toHaveBeenCalledWith(
|
||||
"user-3",
|
||||
{ ...domainSettingsStateDefinition, key: "defaultUriMatchStrategy" },
|
||||
null,
|
||||
);
|
||||
});
|
||||
|
||||
it("should add explicit value back to accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).toHaveBeenCalledTimes(4);
|
||||
expect(helper.set).toHaveBeenCalledWith("global", {
|
||||
neverDomains: mockNeverDomains,
|
||||
otherStuff: "otherStuff1",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-1", {
|
||||
settings: {
|
||||
defaultUriMatch: 3,
|
||||
settings: {
|
||||
equivalentDomains: [] as string[][],
|
||||
},
|
||||
otherStuff: "otherStuff2",
|
||||
},
|
||||
otherStuff: "otherStuff3",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-2", {
|
||||
settings: {
|
||||
settings: {
|
||||
equivalentDomains: [["apple.com", "icloud.com"]],
|
||||
},
|
||||
otherStuff: "otherStuff4",
|
||||
},
|
||||
otherStuff: "otherStuff5",
|
||||
});
|
||||
expect(helper.set).toHaveBeenCalledWith("user-3", {
|
||||
settings: {
|
||||
defaultUriMatch: 1,
|
||||
otherStuff: "otherStuff6",
|
||||
},
|
||||
otherStuff: "otherStuff7",
|
||||
});
|
||||
});
|
||||
|
||||
it("should not try to restore values to missing accounts", async () => {
|
||||
await sut.rollback(helper);
|
||||
|
||||
expect(helper.set).not.toHaveBeenCalledWith("user-4", any());
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,167 @@
|
||||
import { KeyDefinitionLike, MigrationHelper } from "../migration-helper";
|
||||
import { Migrator } from "../migrator";
|
||||
|
||||
const UriMatchStrategy = {
|
||||
Domain: 0,
|
||||
Host: 1,
|
||||
StartsWith: 2,
|
||||
Exact: 3,
|
||||
RegularExpression: 4,
|
||||
Never: 5,
|
||||
} as const;
|
||||
|
||||
type UriMatchStrategySetting = (typeof UriMatchStrategy)[keyof typeof UriMatchStrategy];
|
||||
|
||||
type ExpectedAccountState = {
|
||||
settings?: {
|
||||
defaultUriMatch?: UriMatchStrategySetting;
|
||||
settings?: {
|
||||
equivalentDomains?: string[][];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
type ExpectedGlobalState = {
|
||||
neverDomains?: { [key: string]: null };
|
||||
};
|
||||
|
||||
const defaultUriMatchStrategyDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "defaultUriMatchStrategy",
|
||||
};
|
||||
|
||||
const equivalentDomainsDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "equivalentDomains",
|
||||
};
|
||||
|
||||
const neverDomainsDefinition: KeyDefinitionLike = {
|
||||
stateDefinition: {
|
||||
name: "domainSettings",
|
||||
},
|
||||
key: "neverDomains",
|
||||
};
|
||||
|
||||
export class DomainSettingsMigrator extends Migrator<33, 34> {
|
||||
async migrate(helper: MigrationHelper): Promise<void> {
|
||||
let updateAccount = false;
|
||||
|
||||
// global state ("neverDomains")
|
||||
const globalState = await helper.get<ExpectedGlobalState>("global");
|
||||
|
||||
if (globalState?.neverDomains != null) {
|
||||
await helper.setToGlobal(neverDomainsDefinition, globalState.neverDomains);
|
||||
|
||||
// delete `neverDomains` from state global
|
||||
delete globalState.neverDomains;
|
||||
|
||||
await helper.set<ExpectedGlobalState>("global", globalState);
|
||||
}
|
||||
|
||||
// account state ("defaultUriMatch" and "settings.equivalentDomains")
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]);
|
||||
|
||||
// migrate account state
|
||||
async function migrateAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
const accountSettings = account?.settings;
|
||||
|
||||
if (accountSettings?.defaultUriMatch != undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
defaultUriMatchStrategyDefinition,
|
||||
accountSettings.defaultUriMatch,
|
||||
);
|
||||
delete account.settings.defaultUriMatch;
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (accountSettings?.settings?.equivalentDomains != undefined) {
|
||||
await helper.setToUser(
|
||||
userId,
|
||||
equivalentDomainsDefinition,
|
||||
accountSettings.settings.equivalentDomains,
|
||||
);
|
||||
delete account.settings.settings.equivalentDomains;
|
||||
delete account.settings.settings;
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (updateAccount) {
|
||||
// update the state account settings with the migrated values deleted
|
||||
await helper.set(userId, account);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async rollback(helper: MigrationHelper): Promise<void> {
|
||||
let updateAccount = false;
|
||||
|
||||
// global state ("neverDomains")
|
||||
const globalState = (await helper.get<ExpectedGlobalState>("global")) || {};
|
||||
const neverDomains: { [key: string]: null } =
|
||||
await helper.getFromGlobal(neverDomainsDefinition);
|
||||
|
||||
if (neverDomains != null) {
|
||||
await helper.set<ExpectedGlobalState>("global", {
|
||||
...globalState,
|
||||
neverDomains: neverDomains,
|
||||
});
|
||||
|
||||
// remove the global state provider framework key for `neverDomains`
|
||||
await helper.setToGlobal(neverDomainsDefinition, null);
|
||||
}
|
||||
|
||||
// account state ("defaultUriMatchStrategy" and "equivalentDomains")
|
||||
const accounts = await helper.getAccounts<ExpectedAccountState>();
|
||||
|
||||
await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]);
|
||||
|
||||
// rollback account state
|
||||
async function rollbackAccount(userId: string, account: ExpectedAccountState): Promise<void> {
|
||||
let settings = account?.settings || {};
|
||||
|
||||
const defaultUriMatchStrategy: UriMatchStrategySetting = await helper.getFromUser(
|
||||
userId,
|
||||
defaultUriMatchStrategyDefinition,
|
||||
);
|
||||
|
||||
const equivalentDomains: string[][] = await helper.getFromUser(
|
||||
userId,
|
||||
equivalentDomainsDefinition,
|
||||
);
|
||||
|
||||
// update new settings and remove the account state provider framework keys for the rolled back values
|
||||
if (defaultUriMatchStrategy != null) {
|
||||
settings = { ...settings, defaultUriMatch: defaultUriMatchStrategy };
|
||||
|
||||
await helper.setToUser(userId, defaultUriMatchStrategyDefinition, null);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
if (equivalentDomains != null) {
|
||||
settings = { ...settings, settings: { equivalentDomains } };
|
||||
|
||||
await helper.setToUser(userId, equivalentDomainsDefinition, null);
|
||||
|
||||
updateAccount = true;
|
||||
}
|
||||
|
||||
// commit updated settings to state
|
||||
if (updateAccount) {
|
||||
await helper.set(userId, {
|
||||
...account,
|
||||
settings,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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<CipherView[]>;
|
||||
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>;
|
||||
/**
|
||||
|
@ -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";
|
||||
|
@ -1,8 +0,0 @@
|
||||
export enum UriMatchType {
|
||||
Domain = 0,
|
||||
Host = 1,
|
||||
StartsWith = 2,
|
||||
Exact = 3,
|
||||
RegularExpression = 4,
|
||||
Never = 5,
|
||||
}
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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",
|
||||
|
@ -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<LoginUri>);
|
||||
|
||||
expect(actual).toEqual({
|
||||
uri: "myUri_fromJSON",
|
||||
uriChecksum: "myUriChecksum_fromJSON",
|
||||
match: UriMatchType.Domain,
|
||||
match: UriMatchStrategy.Domain,
|
||||
});
|
||||
expect(actual).toBeInstanceOf(LoginUri);
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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",
|
||||
|
@ -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;
|
||||
|
@ -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<string>,
|
||||
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;
|
||||
|
@ -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<string>,
|
||||
defaultUriMatch: UriMatchType = null,
|
||||
defaultUriMatch: UriMatchStrategySetting = null,
|
||||
): boolean {
|
||||
if (this.uris == null) {
|
||||
return false;
|
||||
|
@ -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<CryptoService>();
|
||||
const stateService = mock<StateService>();
|
||||
const autofillSettingsService = mock<AutofillSettingsService>();
|
||||
const settingsService = mock<SettingsService>();
|
||||
const domainSettingsService = mock<DomainSettingsService>();
|
||||
const apiService = mock<ApiService>();
|
||||
const cipherFileUploadService = mock<CipherFileUploadService>();
|
||||
const i18nService = mock<I18nService>();
|
||||
@ -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,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
@ -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<CipherView[]> {
|
||||
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<any> {
|
||||
|
@ -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<AuthService>;
|
||||
let stateService!: MockProxy<StateService>;
|
||||
let vaultSettingsService: MockProxy<VaultSettingsService>;
|
||||
let domainSettingsService: MockProxy<DomainSettingsService>;
|
||||
let client!: Fido2ClientService;
|
||||
let tab!: chrome.tabs.Tab;
|
||||
|
||||
@ -43,6 +45,7 @@ describe("FidoAuthenticatorService", () => {
|
||||
authService = mock<AuthService>();
|
||||
stateService = mock<StateService>();
|
||||
vaultSettingsService = mock<VaultSettingsService>();
|
||||
domainSettingsService = mock<DomainSettingsService>();
|
||||
|
||||
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);
|
||||
|
||||
|
@ -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$);
|
||||
|
@ -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[]) {
|
||||
|
Loading…
Reference in New Issue
Block a user