From 952b71f4dad68f9c509957ba038bd3886f73c3b5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 19 Mar 2024 16:15:52 -0500 Subject: [PATCH 01/17] [deps] SM: Update eslint-plugin-tailwindcss to v3.15.1 (#8388) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package-lock.json | 8 ++++---- package.json | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index 53f31340b33..ba95074d898 100644 --- a/package-lock.json +++ b/package-lock.json @@ -140,7 +140,7 @@ "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.14.3", + "eslint-plugin-tailwindcss": "3.15.1", "gulp": "4.0.2", "gulp-filter": "9.0.1", "gulp-if": "3.0.0", @@ -18826,9 +18826,9 @@ } }, "node_modules/eslint-plugin-tailwindcss": { - "version": "3.14.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.14.3.tgz", - "integrity": "sha512-1MKT8CrVuqVJleHxb7ICHsF2QwO0G+VJ28athTtlcOkccp0qmwK7nCUa1C9paCZ+VVgQU4fonsjLz/wUxoMHJQ==", + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-tailwindcss/-/eslint-plugin-tailwindcss-3.15.1.tgz", + "integrity": "sha512-4RXRMIaMG07C2TBEW1k0VM4+dDazz1kxcZhkK4zirvmHGZTA4jnlSO2kq5mamuSPi+Wo17dh2SlC8IyFBuCd7Q==", "dev": true, "dependencies": { "fast-glob": "^3.2.5", diff --git a/package.json b/package.json index ec0f1d64747..d201eebcda5 100644 --- a/package.json +++ b/package.json @@ -101,7 +101,7 @@ "eslint-plugin-rxjs": "5.0.3", "eslint-plugin-rxjs-angular": "2.0.1", "eslint-plugin-storybook": "0.8.0", - "eslint-plugin-tailwindcss": "3.14.3", + "eslint-plugin-tailwindcss": "3.15.1", "gulp": "4.0.2", "gulp-filter": "9.0.1", "gulp-if": "3.0.0", From 3953318c2875ed5d2b9c8231095b54db0cf51ee3 Mon Sep 17 00:00:00 2001 From: Cesar Gonzalez Date: Tue, 19 Mar 2024 16:17:10 -0500 Subject: [PATCH 02/17] [PM-6546] Fix issue with blurring of elements after autofill occurs (#8153) * [PM-6546] Fix issue with blurring of elements after autofill occurs * [PM-6546] Implementing a methodology where Firefox browsers render the overlay UI within a div element rather than custom web component --- .../autofill-overlay-button-iframe.spec.ts | 15 ++++-- .../autofill-overlay-button-iframe.ts | 3 +- .../autofill-overlay-iframe-element.spec.ts | 16 ++++++- .../autofill-overlay-iframe-element.ts | 7 ++- .../autofill-overlay-list-iframe.spec.ts | 15 ++++-- .../autofill-overlay-list-iframe.ts | 3 +- .../autofill-overlay-content.service.spec.ts | 38 +++++++++++++++ .../autofill-overlay-content.service.ts | 48 +++++++++++++++---- .../insert-autofill-content.service.spec.ts | 16 ++++++- .../insert-autofill-content.service.ts | 16 ++++--- 10 files changed, 145 insertions(+), 32 deletions(-) diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts index 8106f2698dc..5cba4e0c0e2 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.spec.ts @@ -1,8 +1,15 @@ import AutofillOverlayButtonIframe from "./autofill-overlay-button-iframe"; -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; describe("AutofillOverlayButtonIframe", () => { - window.customElements.define("autofill-overlay-button-iframe", AutofillOverlayButtonIframe); + window.customElements.define( + "autofill-overlay-button-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayButtonIframe", () => { const iframe = document.querySelector("autofill-overlay-button-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayButtonIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts index 813d8054704..4f5d64b3cb8 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-button-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayButtonIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/button.html", AutofillOverlayPort.Button, { diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts index 7af5d973a92..71f7a290de1 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.spec.ts @@ -4,7 +4,21 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; jest.mock("./autofill-overlay-iframe.service"); describe("AutofillOverlayIframeElement", () => { - window.customElements.define("autofill-overlay-iframe", AutofillOverlayIframeElement); + window.customElements.define( + "autofill-overlay-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayIframeElement( + this, + "overlay/button.html", + "overlay/button", + { background: "transparent", border: "none" }, + "bitwardenOverlayButton", + ); + } + }, + ); afterAll(() => { jest.clearAllMocks(); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts index 209834410f9..ed61c1eb8f1 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-iframe-element.ts @@ -1,16 +1,15 @@ import AutofillOverlayIframeService from "./autofill-overlay-iframe.service"; -class AutofillOverlayIframeElement extends HTMLElement { +class AutofillOverlayIframeElement { constructor( + element: HTMLElement, iframePath: string, portName: string, initStyles: Partial, iframeTitle: string, ariaAlert?: string, ) { - super(); - - const shadow: ShadowRoot = this.attachShadow({ mode: "closed" }); + const shadow: ShadowRoot = element.attachShadow({ mode: "closed" }); const autofillOverlayIframeService = new AutofillOverlayIframeService( iframePath, portName, diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts index 5ba39eb0023..ec89697e75e 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.spec.ts @@ -1,8 +1,15 @@ -import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; import AutofillOverlayListIframe from "./autofill-overlay-list-iframe"; describe("AutofillOverlayListIframe", () => { - window.customElements.define("autofill-overlay-list-iframe", AutofillOverlayListIframe); + window.customElements.define( + "autofill-overlay-list-iframe", + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); afterAll(() => { jest.clearAllMocks(); @@ -13,7 +20,7 @@ describe("AutofillOverlayListIframe", () => { const iframe = document.querySelector("autofill-overlay-list-iframe"); - expect(iframe).toBeInstanceOf(AutofillOverlayListIframe); - expect(iframe).toBeInstanceOf(AutofillOverlayIframeElement); + expect(iframe).toBeInstanceOf(HTMLElement); + expect(iframe.shadowRoot).toBeDefined(); }); }); diff --git a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts index b60b618e4e7..23df6581546 100644 --- a/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts +++ b/apps/browser/src/autofill/overlay/iframe-content/autofill-overlay-list-iframe.ts @@ -3,8 +3,9 @@ import { AutofillOverlayPort } from "../../utils/autofill-overlay.enum"; import AutofillOverlayIframeElement from "./autofill-overlay-iframe-element"; class AutofillOverlayListIframe extends AutofillOverlayIframeElement { - constructor() { + constructor(element: HTMLElement) { super( + element, "overlay/list.html", AutofillOverlayPort.List, { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts index 8926f5b298e..9f3ffea142a 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.spec.ts @@ -877,6 +877,44 @@ describe("AutofillOverlayContentService", () => { sender: "autofillOverlayContentService", }); }); + + it("builds the overlay elements as custom web components if the user's browser is not Firefox", () => { + let namesIndex = 0; + const customNames = ["op-autofill-overlay-button", "op-autofill-overlay-list"]; + + jest + .spyOn(autofillOverlayContentService as any, "generateRandomCustomElementName") + .mockImplementation(() => { + if (namesIndex > 1) { + return ""; + } + const customName = customNames[namesIndex]; + namesIndex++; + + return customName; + }); + autofillOverlayContentService["isFirefoxBrowser"] = false; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayButtonElement"].tagName).toEqual( + customNames[0].toUpperCase(), + ); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLElement); + expect(autofillOverlayContentService["overlayListElement"].tagName).toEqual( + customNames[1].toUpperCase(), + ); + }); + + it("builds the overlay elements as `div` elements if the user's browser is Firefox", () => { + autofillOverlayContentService["isFirefoxBrowser"] = true; + + autofillOverlayContentService.openAutofillOverlay(); + + expect(autofillOverlayContentService["overlayButtonElement"]).toBeInstanceOf(HTMLDivElement); + expect(autofillOverlayContentService["overlayListElement"]).toBeInstanceOf(HTMLDivElement); + }); }); describe("focusMostRecentOverlayField", () => { diff --git a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts index 2cf063a5ba8..79abdc39381 100644 --- a/apps/browser/src/autofill/services/autofill-overlay-content.service.ts +++ b/apps/browser/src/autofill/services/autofill-overlay-content.service.ts @@ -30,6 +30,10 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte isOverlayCiphersPopulated = false; pageDetailsUpdateRequired = false; autofillOverlayVisibility: number; + private isFirefoxBrowser = + globalThis.navigator.userAgent.indexOf(" Firefox/") !== -1 || + globalThis.navigator.userAgent.indexOf(" Gecko/") !== -1; + private readonly generateRandomCustomElementName = generateRandomCustomElementName; private readonly findTabs = tabbable; private readonly sendExtensionMessage = sendExtensionMessage; private formFieldElements: Set> = new Set([]); @@ -593,6 +597,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayButtonPosition() { if (!this.overlayButtonElement) { this.createAutofillOverlayButton(); + this.updateCustomElementDefaultStyles(this.overlayButtonElement); } if (!this.isOverlayButtonVisible) { @@ -613,6 +618,7 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte private updateOverlayListPosition() { if (!this.overlayListElement) { this.createAutofillOverlayList(); + this.updateCustomElementDefaultStyles(this.overlayListElement); } if (!this.isOverlayListVisible) { @@ -765,11 +771,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayButtonIframe); - this.overlayButtonElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayButtonElement = globalThis.document.createElement("div"); + new AutofillOverlayButtonIframe(this.overlayButtonElement); - this.updateCustomElementDefaultStyles(this.overlayButtonElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayButtonIframe(this); + } + }, + ); + this.overlayButtonElement = globalThis.document.createElement(customElementName); } /** @@ -781,11 +800,24 @@ class AutofillOverlayContentService implements AutofillOverlayContentServiceInte return; } - const customElementName = generateRandomCustomElementName(); - globalThis.customElements?.define(customElementName, AutofillOverlayListIframe); - this.overlayListElement = globalThis.document.createElement(customElementName); + if (this.isFirefoxBrowser) { + this.overlayListElement = globalThis.document.createElement("div"); + new AutofillOverlayListIframe(this.overlayListElement); - this.updateCustomElementDefaultStyles(this.overlayListElement); + return; + } + + const customElementName = this.generateRandomCustomElementName(); + globalThis.customElements?.define( + customElementName, + class extends HTMLElement { + constructor() { + super(); + new AutofillOverlayListIframe(this); + } + }, + ); + this.overlayListElement = globalThis.document.createElement(customElementName); } /** diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts index c63d25c3646..5ea1284d1bb 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.spec.ts @@ -553,17 +553,30 @@ describe("InsertAutofillContentService", () => { insertAutofillContentService as any, "simulateUserMouseClickAndFocusEventInteractions", ); + jest.spyOn(targetInput, "blur"); insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); expect( insertAutofillContentService["collectAutofillContentService"].getAutofillFieldElementByOpid, ).toBeCalledWith("__0"); + expect(targetInput.blur).not.toHaveBeenCalled(); expect( insertAutofillContentService["simulateUserMouseClickAndFocusEventInteractions"], ).toHaveBeenCalledWith(targetInput, true); expect(elementEventCount).toEqual(expectedElementEventCount); }); + + it("blurs the element if it is currently the active element before simulating click and focus events", () => { + const targetInput = document.querySelector('input[type="text"]') as FormElementWithAttribute; + targetInput.opid = "__0"; + targetInput.focus(); + jest.spyOn(targetInput, "blur"); + + insertAutofillContentService["handleFocusOnFieldByOpidAction"]("__0"); + + expect(targetInput.blur).toHaveBeenCalled(); + }); }); describe("insertValueIntoField", () => { @@ -710,7 +723,7 @@ describe("InsertAutofillContentService", () => { }); describe("triggerPostInsertEventsOnElement", () => { - it("triggers simulated event interactions and blurs the element after", () => { + it("triggers simulated event interactions", () => { const elementValue = "test"; document.body.innerHTML = ``; const element = document.getElementById("username") as FillableFormFieldElement; @@ -726,7 +739,6 @@ describe("InsertAutofillContentService", () => { expect(insertAutofillContentService["simulateInputElementChangedEvent"]).toHaveBeenCalledWith( element, ); - expect(element.blur).toHaveBeenCalled(); expect(element.value).toBe(elementValue); }); }); diff --git a/apps/browser/src/autofill/services/insert-autofill-content.service.ts b/apps/browser/src/autofill/services/insert-autofill-content.service.ts index c5b763d77df..dd14cadfa7b 100644 --- a/apps/browser/src/autofill/services/insert-autofill-content.service.ts +++ b/apps/browser/src/autofill/services/insert-autofill-content.service.ts @@ -185,11 +185,18 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf /** * Handles finding an element by opid and triggering click and focus events on the element. - * @param {string} opid - * @private + * To ensure that we trigger a blur event correctly on a filled field, we first check if the + * element is already focused. If it is, we blur the element before focusing on it again. + * + * @param {string} opid - The opid of the element to focus on. */ private handleFocusOnFieldByOpidAction(opid: string) { const element = this.collectAutofillContentService.getAutofillFieldElementByOpid(opid); + + if (document.activeElement === element) { + element.blur(); + } + this.simulateUserMouseClickAndFocusEventInteractions(element, true); } @@ -282,7 +289,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf } this.simulateInputElementChangedEvent(element); - element.blur(); } /** @@ -379,10 +385,6 @@ class InsertAutofillContentService implements InsertAutofillContentServiceInterf element.dispatchEvent(new Event(simulatedInputEvents[index], { bubbles: true })); } } - - private nodeIsElement(node: Node): node is HTMLElement { - return node.nodeType === Node.ELEMENT_NODE; - } } export default InsertAutofillContentService; From bf2d2cfbed3954efffa1d88f07af5e24da659078 Mon Sep 17 00:00:00 2001 From: Addison Beck Date: Tue, 19 Mar 2024 16:37:35 -0500 Subject: [PATCH 03/17] Migrate `autoConfirmFingerPrints` to `StateProvider` (#8337) * Fix a typo in the `StateDefinition` description * Introduce `OrganizationManagementPreferencesService` * Declare `OrganizationManagementPreferencesService` in DI * Update `autoConfirmFingerPrints` logic in emergency access files * Update `autoConfirmFingerPrints` logic in `people` files * Remove `autoConfirmFingerPrints` from `StateService` and `Account` * Migrate existing client data for `autoConfirmFingerPrints` * Update apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts Co-authored-by: Matt Gibson * Update apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts Co-authored-by: Matt Gibson * Use `set` instead of `update` for function names --------- Co-authored-by: Matt Gibson --- .../common/base.people.component.ts | 9 +- .../manage/user-confirm.component.ts | 6 +- .../organizations/members/people.component.ts | 6 +- .../emergency-access-confirm.component.ts | 6 +- .../emergency-access.component.ts | 6 +- .../providers/manage/people.component.ts | 6 +- .../src/services/jslib-services.module.ts | 7 ++ ...nization-management-preferences.service.ts | 22 ++++ ...ion-management-preferences.service.spec.ts | 44 ++++++++ ...nization-management-preferences.service.ts | 71 ++++++++++++ .../platform/abstractions/state.service.ts | 3 - .../src/platform/models/domain/account.ts | 1 - .../src/platform/services/state.service.ts | 18 ---- .../src/platform/state/state-definition.ts | 6 +- .../src/platform/state/state-definitions.ts | 7 ++ libs/common/src/state-migrations/migrate.ts | 6 +- ...rm-finger-prints-to-state-provider.spec.ts | 102 ++++++++++++++++++ ...confirm-finger-prints-to-state-provider.ts | 63 +++++++++++ 18 files changed, 346 insertions(+), 43 deletions(-) create mode 100644 libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts create mode 100644 libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts create mode 100644 libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts create mode 100644 libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts diff --git a/apps/web/src/app/admin-console/common/base.people.component.ts b/apps/web/src/app/admin-console/common/base.people.component.ts index 29303f4ccc2..0a1f4338ff8 100644 --- a/apps/web/src/app/admin-console/common/base.people.component.ts +++ b/apps/web/src/app/admin-console/common/base.people.component.ts @@ -1,10 +1,12 @@ import { Directive, ViewChild, ViewContainerRef } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserStatusType, OrganizationUserType, @@ -17,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; @@ -109,8 +110,8 @@ export abstract class BasePeopleComponent< private logService: LogService, private searchPipe: SearchPipe, protected userNamePipe: UserNamePipe, - protected stateService: StateService, protected dialogService: DialogService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) {} abstract edit(user: UserType): void; @@ -351,7 +352,9 @@ export abstract class BasePeopleComponent< const publicKeyResponse = await this.apiService.getUserPublicKey(user.userId); const publicKey = Utils.fromB64ToArray(publicKeyResponse.publicKey); - const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); + const autoConfirm = await firstValueFrom( + this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); if (autoConfirm == null || !autoConfirm) { const [modal] = await this.modalService.openViewRef( UserConfirmComponent, diff --git a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts index 25f16aff65d..4f712f30a81 100644 --- a/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/user-confirm.component.ts @@ -1,8 +1,8 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; @Component({ selector: "app-user-confirm", @@ -22,7 +22,7 @@ export class UserConfirmComponent implements OnInit { constructor( private cryptoService: CryptoService, private logService: LogService, - private stateService: StateService, + private organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) {} async ngOnInit() { @@ -45,7 +45,7 @@ export class UserConfirmComponent implements OnInit { } if (this.dontAskAgain) { - await this.stateService.setAutoConfirmFingerprints(true); + await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } this.onConfirmedUser.emit(); diff --git a/apps/web/src/app/admin-console/organizations/members/people.component.ts b/apps/web/src/app/admin-console/organizations/members/people.component.ts index b3142125dfe..b2aedacc800 100644 --- a/apps/web/src/app/admin-console/organizations/members/people.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/people.component.ts @@ -21,6 +21,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { OrganizationUserConfirmRequest } from "@bitwarden/common/admin-console/abstractions/organization-user/requests"; import { @@ -43,7 +44,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -117,7 +117,6 @@ export class PeopleComponent searchPipe: SearchPipe, userNamePipe: UserNamePipe, private syncService: SyncService, - stateService: StateService, private organizationService: OrganizationService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, @@ -125,6 +124,7 @@ export class PeopleComponent private router: Router, private groupService: GroupService, private collectionService: CollectionService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { super( apiService, @@ -137,8 +137,8 @@ export class PeopleComponent logService, searchPipe, userNamePipe, - stateService, dialogService, + organizationManagementPreferencesService, ); } diff --git a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts index 4afc60c9be3..3bfe90d48ed 100644 --- a/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/confirm/emergency-access-confirm.component.ts @@ -3,9 +3,9 @@ import { Component, OnInit, Inject } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { DialogService } from "@bitwarden/components"; @@ -36,7 +36,7 @@ export class EmergencyAccessConfirmComponent implements OnInit { private formBuilder: FormBuilder, private apiService: ApiService, private cryptoService: CryptoService, - private stateService: StateService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, private logService: LogService, private dialogRef: DialogRef, ) {} @@ -63,7 +63,7 @@ export class EmergencyAccessConfirmComponent implements OnInit { } if (this.confirmForm.get("dontAskAgain").value) { - await this.stateService.setAutoConfirmFingerprints(true); + await this.organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); } try { diff --git a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts index ce8db4e9313..05e65405fb7 100644 --- a/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts +++ b/apps/web/src/app/auth/settings/emergency-access/emergency-access.component.ts @@ -3,6 +3,7 @@ import { lastValueFrom, Observable, firstValueFrom } from "rxjs"; import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -64,6 +65,7 @@ export class EmergencyAccessComponent implements OnInit { private organizationService: OrganizationService, protected dialogService: DialogService, billingAccountProfileStateService: BillingAccountProfileStateService, + protected organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { this.canAccessPremium$ = billingAccountProfileStateService.hasPremiumFromAnySource$; } @@ -136,7 +138,9 @@ export class EmergencyAccessComponent implements OnInit { return; } - const autoConfirm = await this.stateService.getAutoConfirmFingerPrints(); + const autoConfirm = await firstValueFrom( + this.organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); if (autoConfirm == null || !autoConfirm) { const dialogRef = EmergencyAccessConfirmComponent.open(this.dialogService, { data: { diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts index 227e37984fd..b83daf24b52 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/manage/people.component.ts @@ -7,6 +7,7 @@ import { UserNamePipe } from "@bitwarden/angular/pipes/user-name.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { ProviderService } from "@bitwarden/common/admin-console/abstractions/provider.service"; import { ProviderUserStatusType, ProviderUserType } from "@bitwarden/common/admin-console/enums"; import { ProviderUserBulkRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-user-bulk.request"; @@ -18,7 +19,6 @@ import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.se import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; import { DialogService } from "@bitwarden/components"; import { BasePeopleComponent } from "@bitwarden/web-vault/app/admin-console/common/base.people.component"; @@ -67,9 +67,9 @@ export class PeopleComponent logService: LogService, searchPipe: SearchPipe, userNamePipe: UserNamePipe, - stateService: StateService, private providerService: ProviderService, dialogService: DialogService, + organizationManagementPreferencesService: OrganizationManagementPreferencesService, ) { super( apiService, @@ -82,8 +82,8 @@ export class PeopleComponent logService, searchPipe, userNamePipe, - stateService, dialogService, + organizationManagementPreferencesService, ); } diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index a509897fd3a..498c9171b3e 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -27,6 +27,7 @@ import { OrgDomainInternalServiceAbstraction, OrgDomainServiceAbstraction, } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain.service.abstraction"; +import { OrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; import { PolicyApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/policy/policy-api.service.abstraction"; import { @@ -38,6 +39,7 @@ import { OrganizationApiService } from "@bitwarden/common/admin-console/services import { OrganizationService } from "@bitwarden/common/admin-console/services/organization/organization.service"; import { OrgDomainApiService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain-api.service"; import { OrgDomainService } from "@bitwarden/common/admin-console/services/organization-domain/org-domain.service"; +import { DefaultOrganizationManagementPreferencesService } from "@bitwarden/common/admin-console/services/organization-management-preferences/default-organization-management-preferences.service"; import { OrganizationUserServiceImplementation } from "@bitwarden/common/admin-console/services/organization-user/organization-user.service.implementation"; import { PolicyApiService } from "@bitwarden/common/admin-console/services/policy/policy-api.service"; import { PolicyService } from "@bitwarden/common/admin-console/services/policy/policy.service"; @@ -1048,6 +1050,11 @@ const typesafeProviders: Array = [ useClass: DefaultBillingAccountProfileStateService, deps: [ActiveUserStateProvider], }), + safeProvider({ + provide: OrganizationManagementPreferencesService, + useClass: DefaultOrganizationManagementPreferencesService, + deps: [StateProvider], + }), ]; function encryptServiceFactory( diff --git a/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts new file mode 100644 index 00000000000..2328165e4b2 --- /dev/null +++ b/libs/common/src/admin-console/abstractions/organization-management-preferences/organization-management-preferences.service.ts @@ -0,0 +1,22 @@ +import { Observable } from "rxjs"; + +/** + * Manages the state of a single organization management preference. + * Can be used to subscribe to or update a given property. + */ +export class OrganizationManagementPreference { + state$: Observable; + set: (value: T) => Promise; + + constructor(state$: Observable, setFn: (value: T) => Promise) { + this.state$ = state$; + this.set = setFn; + } +} + +/** + * Publishes state of a given user's personal settings relating to the user experience of managing an organization. + */ +export abstract class OrganizationManagementPreferencesService { + autoConfirmFingerPrints: OrganizationManagementPreference; +} diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts new file mode 100644 index 00000000000..0d16e770eae --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.spec.ts @@ -0,0 +1,44 @@ +import { MockProxy } from "jest-mock-extended"; +import { firstValueFrom } from "rxjs"; + +import { FakeStateProvider, mockAccountServiceWith } from "../../../../spec"; +import { UserId } from "../../../types/guid"; + +import { DefaultOrganizationManagementPreferencesService } from "./default-organization-management-preferences.service"; + +describe("OrganizationManagementPreferencesService", () => { + let stateProvider: FakeStateProvider; + let organizationManagementPreferencesService: MockProxy; + + beforeEach(() => { + const accountService = mockAccountServiceWith("userId" as UserId); + stateProvider = new FakeStateProvider(accountService); + organizationManagementPreferencesService = new DefaultOrganizationManagementPreferencesService( + stateProvider, + ); + }); + + describe("autoConfirmFingerPrints", () => { + it("returns false by default", async () => { + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + it("returns true if set", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(true); + }); + it("can be unset", async () => { + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(true); + await organizationManagementPreferencesService.autoConfirmFingerPrints.set(false); + const value = await firstValueFrom( + organizationManagementPreferencesService.autoConfirmFingerPrints.state$, + ); + expect(value).toEqual(false); + }); + }); +}); diff --git a/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts new file mode 100644 index 00000000000..e257b691638 --- /dev/null +++ b/libs/common/src/admin-console/services/organization-management-preferences/default-organization-management-preferences.service.ts @@ -0,0 +1,71 @@ +import { map } from "rxjs"; +import { Jsonify } from "type-fest"; + +import { + ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, + StateProvider, + UserKeyDefinition, +} from "../../../platform/state"; +import { + OrganizationManagementPreference, + OrganizationManagementPreferencesService, +} from "../../abstractions/organization-management-preferences/organization-management-preferences.service"; + +/** + * This helper function can be used to quickly create `KeyDefinitions` that + * target the `ORGANIZATION_MANAGEMENT_PREFERENCES_DISK` `StateDefinition` + * and that have the default deserializer and `clearOn` options. Any + * contenders for options to add to this service will likely use these same + * options. + */ +function buildKeyDefinition(key: string): UserKeyDefinition { + return new UserKeyDefinition(ORGANIZATION_MANAGEMENT_PREFERENCES_DISK, key, { + deserializer: (obj: Jsonify) => obj as T, + clearOn: ["logout"], + }); +} + +export const AUTO_CONFIRM_FINGERPRINTS = buildKeyDefinition("autoConfirmFingerPrints"); + +export class DefaultOrganizationManagementPreferencesService + implements OrganizationManagementPreferencesService +{ + constructor(private stateProvider: StateProvider) {} + + autoConfirmFingerPrints = this.buildOrganizationManagementPreference( + AUTO_CONFIRM_FINGERPRINTS, + false, + ); + + /** + * Returns an `OrganizationManagementPreference` object for the provided + * `KeyDefinition`. This object can then be used by callers to subscribe to + * a given key, or set its value in state. + */ + private buildOrganizationManagementPreference( + keyDefinition: UserKeyDefinition, + defaultValue: T, + ) { + return new OrganizationManagementPreference( + this.getKeyFromState(keyDefinition).state$.pipe(map((x) => x ?? defaultValue)), + this.setKeyInStateFn(keyDefinition), + ); + } + + /** + * Returns the full `ActiveUserState` value for a given `keyDefinition` + * The returned value can then be called for subscription || set operations + */ + private getKeyFromState(keyDefinition: UserKeyDefinition) { + return this.stateProvider.getActive(keyDefinition); + } + + /** + * Returns a function that can be called to set the given `keyDefinition` in state + */ + private setKeyInStateFn(keyDefinition: UserKeyDefinition) { + return async (value: T) => { + await this.getKeyFromState(keyDefinition).update(() => value); + }; + } +} diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 3413afe1825..3bed46e769a 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -54,9 +54,6 @@ export abstract class StateService { setAddEditCipherInfo: (value: AddEditCipherInfo, options?: StorageOptions) => Promise; getAlwaysShowDock: (options?: StorageOptions) => Promise; setAlwaysShowDock: (value: boolean, options?: StorageOptions) => Promise; - - getAutoConfirmFingerPrints: (options?: StorageOptions) => Promise; - setAutoConfirmFingerprints: (value: boolean, options?: StorageOptions) => Promise; getBiometricFingerprintValidated: (options?: StorageOptions) => Promise; setBiometricFingerprintValidated: (value: boolean, options?: StorageOptions) => Promise; getConvertAccountToKeyConnector: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 07efb505a5e..460ce25c15e 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -187,7 +187,6 @@ export class AccountProfile { } export class AccountSettings { - autoConfirmFingerPrints?: boolean; defaultUriMatch?: UriMatchStrategySetting; disableGa?: boolean; enableAlwaysOnTop?: boolean; diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 31d69e868bf..0985c9949a5 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -300,24 +300,6 @@ export class StateService< ); } - async getAutoConfirmFingerPrints(options?: StorageOptions): Promise { - return ( - (await this.getAccount(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) - ?.settings?.autoConfirmFingerPrints ?? false - ); - } - - async setAutoConfirmFingerprints(value: boolean, options?: StorageOptions): Promise { - const account = await this.getAccount( - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - account.settings.autoConfirmFingerPrints = value; - await this.saveAccount( - account, - this.reconcileOptions(options, await this.defaultOnDiskOptions()), - ); - } - async getBiometricFingerprintValidated(options?: StorageOptions): Promise { return ( (await this.getGlobals(this.reconcileOptions(options, await this.defaultOnDiskOptions()))) diff --git a/libs/common/src/platform/state/state-definition.ts b/libs/common/src/platform/state/state-definition.ts index 858be39855a..15dc9ff7574 100644 --- a/libs/common/src/platform/state/state-definition.ts +++ b/libs/common/src/platform/state/state-definition.ts @@ -2,9 +2,9 @@ * Default storage location options. * * `disk` generally means state that is accessible between restarts of the application, - * with the exception of the web client. In web this means `sessionStorage`. The data is - * through refreshes of the page but not available once that tab is closed or from any - * other tabs. + * with the exception of the web client. In web this means `sessionStorage`. The data + * persists through refreshes of the page but not available once that tab is closed or + * from any other tabs. * * `memory` means that the information stored there goes away during application * restarts. diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 8115555b2ed..122578d2325 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -22,6 +22,13 @@ import { StateDefinition } from "./state-definition"; export const ORGANIZATIONS_DISK = new StateDefinition("organizations", "disk"); export const POLICIES_DISK = new StateDefinition("policies", "disk"); export const PROVIDERS_DISK = new StateDefinition("providers", "disk"); +export const ORGANIZATION_MANAGEMENT_PREFERENCES_DISK = new StateDefinition( + "organizationManagementPreferences", + "disk", + { + web: "disk-local", + }, +); // Billing export const BILLING_DISK = new StateDefinition("billing", "disk"); diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 000f08b392b..1e3b925c2b2 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -38,6 +38,7 @@ import { RemoveEverBeenUnlockedMigrator } from "./migrations/4-remove-ever-been- import { OrganizationMigrator } from "./migrations/40-move-organization-state-to-state-provider"; import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider"; import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider"; +import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -46,7 +47,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 42; +export const CURRENT_VERSION = 43; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -90,7 +91,8 @@ export function createMigrationBuilder() { .with(MoveBillingAccountProfileMigrator, 38, 39) .with(OrganizationMigrator, 39, 40) .with(EventCollectionMigrator, 40, 41) - .with(EnableFaviconMigrator, 41, 42); + .with(EnableFaviconMigrator, 41, 42) + .with(AutoConfirmFingerPrintsMigrator, 42, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts new file mode 100644 index 00000000000..359f582b8c0 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.spec.ts @@ -0,0 +1,102 @@ +import { MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper, runMigrator } from "../migration-helper.spec"; + +import { AutoConfirmFingerPrintsMigrator } from "./43-move-auto-confirm-finger-prints-to-state-provider"; + +function rollbackJSON() { + return { + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }; +} + +describe("AutoConfirmFingerPrintsMigrator", () => { + const migrator = new AutoConfirmFingerPrintsMigrator(42, 43); + + it("should migrate the autoConfirmFingerPrints property from the account settings object to a user StorageKey", async () => { + const output = await runMigrator(migrator, { + authenticatedAccounts: ["user-1", "user-2"] as const, + "user-1": { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + autoConfirmFingerPrints: false, + extra: "data", + }, + extra: "data", + }, + }); + + expect(output).toEqual({ + authenticatedAccounts: ["user-1", "user-2"], + "user_user-1_organizationManagementPreferences_autoConfirmFingerPrints": true, + "user_user-2_organizationManagementPreferences_autoConfirmFingerPrints": false, + "user-1": { + settings: { + extra: "data", + }, + extra: "data", + }, + "user-2": { + settings: { + extra: "data", + }, + extra: "data", + }, + }); + }); + + describe("rollback", () => { + let helper: MockProxy; + let sut: AutoConfirmFingerPrintsMigrator; + + const keyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: { + name: "organizationManagementPreferences", + }, + }; + + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 43); + sut = new AutoConfirmFingerPrintsMigrator(42, 43); + }); + + it("should null the autoConfirmFingerPrints user StorageKey for each account", async () => { + await sut.rollback(helper); + expect(helper.setToUser).toHaveBeenCalledWith("user-1", keyDefinitionLike, null); + }); + + it("should add the autoConfirmFingerPrints property back to the account settings object", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("user-1", { + settings: { + autoConfirmFingerPrints: true, + extra: "data", + }, + extra: "data", + }); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts new file mode 100644 index 00000000000..246e3cf4365 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/43-move-auto-confirm-finger-prints-to-state-provider.ts @@ -0,0 +1,63 @@ +import { KeyDefinitionLike, MigrationHelper, StateDefinitionLike } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type ExpectedAccountState = { + settings?: { autoConfirmFingerPrints?: boolean }; +}; + +const ORGANIZATION_MANAGEMENT_PREFERENCES: StateDefinitionLike = { + name: "organizationManagementPreferences", +}; + +const AUTO_CONFIRM_FINGERPRINTS: KeyDefinitionLike = { + key: "autoConfirmFingerPrints", + stateDefinition: ORGANIZATION_MANAGEMENT_PREFERENCES, +}; + +export class AutoConfirmFingerPrintsMigrator extends Migrator<42, 43> { + async migrate(helper: MigrationHelper): Promise { + const legacyAccounts = await helper.getAccounts(); + + await Promise.all( + legacyAccounts.map(async ({ userId, account }) => { + if (account?.settings?.autoConfirmFingerPrints != null) { + await helper.setToUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + account.settings.autoConfirmFingerPrints, + ); + delete account?.settings?.autoConfirmFingerPrints; + await helper.set(userId, account); + } + }), + ); + } + + async rollback(helper: MigrationHelper): Promise { + async function rollbackUser(userId: string, account: ExpectedAccountState) { + let updatedAccount = false; + const autoConfirmFingerPrints = await helper.getFromUser( + userId, + AUTO_CONFIRM_FINGERPRINTS, + ); + + if (autoConfirmFingerPrints) { + if (!account) { + account = {}; + } + + updatedAccount = true; + account.settings.autoConfirmFingerPrints = autoConfirmFingerPrints; + await helper.setToUser(userId, AUTO_CONFIRM_FINGERPRINTS, null); + } + + if (updatedAccount) { + await helper.set(userId, account); + } + } + + const accounts = await helper.getAccounts(); + + await Promise.all(accounts.map(({ userId, account }) => rollbackUser(userId, account))); + } +} From 4a3e556bbc89a87b862d8a72d8a98da8897a96d2 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Tue, 19 Mar 2024 20:22:46 -0400 Subject: [PATCH 04/17] [PM-4802] Update 2FA recovery link to use the webVaultUrl from environment service (#8020) * Updated the link to use the webVaultUrl from environment service * Updated desktop component. --- apps/browser/src/auth/popup/two-factor-options.component.ts | 4 +++- apps/desktop/src/auth/two-factor-options.component.ts | 4 +++- apps/web/src/app/auth/two-factor-options.component.ts | 4 +++- .../src/auth/components/two-factor-options.component.ts | 5 ++++- 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/browser/src/auth/popup/two-factor-options.component.ts b/apps/browser/src/auth/popup/two-factor-options.component.ts index d3d7b7f33a8..bad2e4a9e77 100644 --- a/apps/browser/src/auth/popup/two-factor-options.component.ts +++ b/apps/browser/src/auth/popup/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,9 +17,10 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, private activatedRoute: ActivatedRoute, ) { - super(twoFactorService, router, i18nService, platformUtilsService, window); + super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); } close() { diff --git a/apps/desktop/src/auth/two-factor-options.component.ts b/apps/desktop/src/auth/two-factor-options.component.ts index c1e4ae51778..624d003c91f 100644 --- a/apps/desktop/src/auth/two-factor-options.component.ts +++ b/apps/desktop/src/auth/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,7 +17,8 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, ) { - super(twoFactorService, router, i18nService, platformUtilsService, window); + super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); } } diff --git a/apps/web/src/app/auth/two-factor-options.component.ts b/apps/web/src/app/auth/two-factor-options.component.ts index c1e4ae51778..624d003c91f 100644 --- a/apps/web/src/app/auth/two-factor-options.component.ts +++ b/apps/web/src/app/auth/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { Router } from "@angular/router"; import { TwoFactorOptionsComponent as BaseTwoFactorOptionsComponent } from "@bitwarden/angular/auth/components/two-factor-options.component"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -16,7 +17,8 @@ export class TwoFactorOptionsComponent extends BaseTwoFactorOptionsComponent { router: Router, i18nService: I18nService, platformUtilsService: PlatformUtilsService, + environmentService: EnvironmentService, ) { - super(twoFactorService, router, i18nService, platformUtilsService, window); + super(twoFactorService, router, i18nService, platformUtilsService, window, environmentService); } } diff --git a/libs/angular/src/auth/components/two-factor-options.component.ts b/libs/angular/src/auth/components/two-factor-options.component.ts index 4293eb9966e..0ad6486158c 100644 --- a/libs/angular/src/auth/components/two-factor-options.component.ts +++ b/libs/angular/src/auth/components/two-factor-options.component.ts @@ -3,6 +3,7 @@ import { Router } from "@angular/router"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; +import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -19,6 +20,7 @@ export class TwoFactorOptionsComponent implements OnInit { protected i18nService: I18nService, protected platformUtilsService: PlatformUtilsService, protected win: Window, + protected environmentService: EnvironmentService, ) {} ngOnInit() { @@ -30,7 +32,8 @@ export class TwoFactorOptionsComponent implements OnInit { } recover() { - this.platformUtilsService.launchUri("https://vault.bitwarden.com/#/recover-2fa"); + const webVault = this.environmentService.getWebVaultUrl(); + this.platformUtilsService.launchUri(webVault + "/#/recover-2fa"); this.onRecoverSelected.emit(); } } From 93d05c68321eeeca4ddc5569232e77977288c291 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 20 Mar 2024 08:23:27 -0500 Subject: [PATCH 05/17] Use `disk-local` for Theme (#8400) --- libs/common/src/platform/state/state-definitions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index 122578d2325..fdd9277c7b5 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -75,7 +75,7 @@ export const CLEAR_EVENT_DISK = new StateDefinition("clearEvent", "disk"); export const CRYPTO_DISK = new StateDefinition("crypto", "disk"); export const CRYPTO_MEMORY = new StateDefinition("crypto", "memory"); export const ENVIRONMENT_DISK = new StateDefinition("environment", "disk"); -export const THEMING_DISK = new StateDefinition("theming", "disk"); +export const THEMING_DISK = new StateDefinition("theming", "disk", { web: "disk-local" }); export const TRANSLATION_DISK = new StateDefinition("translation", "disk"); // Secrets Manager From 72402852bb790a0a8272b72b79b462fbcc80806f Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 20 Mar 2024 10:08:06 -0400 Subject: [PATCH 06/17] dontShowCards and dontShowIdentities values are not inverted during migration to the new state (#8391) --- .../36-move-show-card-and-identity-to-state-provider.spec.ts | 4 ++-- .../36-move-show-card-and-identity-to-state-provider.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts index 64a7fd8efa1..499ceae5fa1 100644 --- a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts +++ b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.spec.ts @@ -87,12 +87,12 @@ describe("VaultSettingsKeyMigrator", () => { expect(helper.setToUser).toHaveBeenCalledWith( "user-1", { ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" }, - true, + false, ); expect(helper.setToUser).toHaveBeenCalledWith( "user-1", { ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" }, - true, + false, ); }); }); diff --git a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts index 572e074cf1c..8e86507a3b8 100644 --- a/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts +++ b/libs/common/src/state-migrations/migrations/36-move-show-card-and-identity-to-state-provider.ts @@ -30,7 +30,7 @@ export class VaultSettingsKeyMigrator extends Migrator<35, 36> { await helper.setToUser( userId, { ...vaultSettingsStateDefinition, key: "showCardsCurrentTab" }, - accountSettings.dontShowCardsCurrentTab, + !accountSettings.dontShowCardsCurrentTab, ); delete account.settings.dontShowCardsCurrentTab; updateAccount = true; @@ -40,7 +40,7 @@ export class VaultSettingsKeyMigrator extends Migrator<35, 36> { await helper.setToUser( userId, { ...vaultSettingsStateDefinition, key: "showIdentitiesCurrentTab" }, - accountSettings.dontShowIdentitiesCurrentTab, + !accountSettings.dontShowIdentitiesCurrentTab, ); delete account.settings.dontShowIdentitiesCurrentTab; updateAccount = true; From 7b40c21798f2ceac4c5c751edcf3f99973188544 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 20 Mar 2024 13:56:37 -0500 Subject: [PATCH 07/17] Filter out invalid encrypted keys state (#8408) --- libs/common/src/platform/services/crypto.service.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/libs/common/src/platform/services/crypto.service.ts b/libs/common/src/platform/services/crypto.service.ts index 86f7c3798f0..fbb6a852937 100644 --- a/libs/common/src/platform/services/crypto.service.ts +++ b/libs/common/src/platform/services/crypto.service.ts @@ -1,5 +1,5 @@ import * as bigInt from "big-integer"; -import { Observable, firstValueFrom, map } from "rxjs"; +import { Observable, filter, firstValueFrom, map } from "rxjs"; import { EncryptedOrganizationKeyData } from "../../admin-console/models/data/encrypted-organization-key.data"; import { ProfileOrganizationResponse } from "../../admin-console/models/response/profile-organization.response"; @@ -100,7 +100,9 @@ export class CryptoService implements CryptoServiceAbstraction { // User Asymmetric Key Pair this.activeUserEncryptedPrivateKeyState = stateProvider.getActive(USER_ENCRYPTED_PRIVATE_KEY); this.activeUserPrivateKeyState = stateProvider.getDerived( - this.activeUserEncryptedPrivateKeyState.combinedState$, + this.activeUserEncryptedPrivateKeyState.combinedState$.pipe( + filter(([_userId, key]) => key != null), + ), USER_PRIVATE_KEY, { encryptService: this.encryptService, @@ -109,7 +111,7 @@ export class CryptoService implements CryptoServiceAbstraction { ); this.activeUserPrivateKey$ = this.activeUserPrivateKeyState.state$; // may be null this.activeUserPublicKeyState = stateProvider.getDerived( - this.activeUserPrivateKey$, + this.activeUserPrivateKey$.pipe(filter((key) => key != null)), USER_PUBLIC_KEY, { cryptoFunctionService: this.cryptoFunctionService, @@ -122,7 +124,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_ENCRYPTED_ORGANIZATION_KEYS, ); this.activeUserOrgKeysState = stateProvider.getDerived( - this.activeUserEncryptedOrgKeysState.state$, + this.activeUserEncryptedOrgKeysState.state$.pipe(filter((keys) => keys != null)), USER_ORGANIZATION_KEYS, { cryptoService: this }, ); @@ -133,7 +135,7 @@ export class CryptoService implements CryptoServiceAbstraction { USER_ENCRYPTED_PROVIDER_KEYS, ); this.activeUserProviderKeysState = stateProvider.getDerived( - this.activeUserEncryptedProviderKeysState.state$, + this.activeUserEncryptedProviderKeysState.state$.pipe(filter((keys) => keys != null)), USER_PROVIDER_KEYS, { encryptService: this.encryptService, cryptoService: this }, ); From 1400ec9c16721ad0dfd9786f20d8a78fa8d43008 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:10:09 -0500 Subject: [PATCH 08/17] [PM-6853] Stop Caching Empty Ciphers List (#8406) * Stop Caching Empty Ciphers List * Allow Caching `null` * Move Logic to CipherService --- libs/common/src/vault/services/cipher.service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index b3deed7c0ca..8a86d9aa050 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -79,7 +79,12 @@ export class CipherService implements CipherServiceAbstraction { } async setDecryptedCipherCache(value: CipherView[]) { - await this.stateService.setDecryptedCiphers(value); + // Sometimes we might prematurely decrypt the vault and that will result in no ciphers + // if we cache it then we may accidentially return it when it's not right, we'd rather try decryption again. + // We still want to set null though, that is the indicator that the cache isn't valid and we should do decryption. + if (value == null || value.length !== 0) { + await this.stateService.setDecryptedCiphers(value); + } if (this.searchService != null) { if (value == null) { this.searchService.clearIndex(); From ec5c6b6797b791cff8460346596362d2b9557bb7 Mon Sep 17 00:00:00 2001 From: Justin Baur <19896123+justindbaur@users.noreply.github.com> Date: Wed, 20 Mar 2024 14:28:22 -0500 Subject: [PATCH 09/17] Create New Method for Setting Avatar Color from Sync (#8403) --- libs/common/src/auth/abstractions/avatar.service.ts | 11 +++++++++++ libs/common/src/auth/services/avatar.service.ts | 4 ++++ libs/common/src/vault/services/sync/sync.service.ts | 3 ++- 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/libs/common/src/auth/abstractions/avatar.service.ts b/libs/common/src/auth/abstractions/avatar.service.ts index 1192ef745df..7da92ac7fdb 100644 --- a/libs/common/src/auth/abstractions/avatar.service.ts +++ b/libs/common/src/auth/abstractions/avatar.service.ts @@ -15,6 +15,17 @@ export abstract class AvatarService { * @returns a promise that resolves when the avatar color is set */ abstract setAvatarColor(color: string): Promise; + /** + * Sets the avatar color for the given user, meant to be used via sync. + * + * @remarks This is meant to be used for getting an updated avatar color from + * the sync endpoint. If the user is changing their avatar color + * on device, you should instead call {@link setAvatarColor}. + * + * @param userId The user id for the user to set the avatar color for + * @param color The color to set the avatar color to + */ + abstract setSyncAvatarColor(userId: UserId, color: string): Promise; /** * Gets the avatar color of the specified user. * diff --git a/libs/common/src/auth/services/avatar.service.ts b/libs/common/src/auth/services/avatar.service.ts index b770dc39b98..9b8c83968dd 100644 --- a/libs/common/src/auth/services/avatar.service.ts +++ b/libs/common/src/auth/services/avatar.service.ts @@ -27,6 +27,10 @@ export class AvatarService implements AvatarServiceAbstraction { await this.stateProvider.setUserState(AVATAR_COLOR, avatarColor); } + async setSyncAvatarColor(userId: UserId, color: string): Promise { + await this.stateProvider.getUser(userId, AVATAR_COLOR).update(() => color); + } + getUserAvatarColor$(userId: UserId): Observable { return this.stateProvider.getUser(userId, AVATAR_COLOR).state$; } diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 1b3e63d0012..654b1137f5d 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -29,6 +29,7 @@ import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; import { InternalSendService } from "../../../tools/send/services/send.service.abstraction"; +import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { FolderApiServiceAbstraction } from "../../../vault/abstractions/folder/folder-api.service.abstraction"; import { InternalFolderService } from "../../../vault/abstractions/folder/folder.service.abstraction"; @@ -313,7 +314,7 @@ export class SyncService implements SyncServiceAbstraction { await this.cryptoService.setPrivateKey(response.privateKey); await this.cryptoService.setProviderKeys(response.providers); await this.cryptoService.setOrgKeys(response.organizations, response.providerOrganizations); - await this.avatarService.setAvatarColor(response.avatarColor); + await this.avatarService.setSyncAvatarColor(response.id as UserId, response.avatarColor); await this.stateService.setSecurityStamp(response.securityStamp); await this.stateService.setEmailVerified(response.emailVerified); From eedf00e215c59382d61343e0ea55c7c3c34ad925 Mon Sep 17 00:00:00 2001 From: Todd Martin <106564991+trmartin4@users.noreply.github.com> Date: Wed, 20 Mar 2024 18:02:45 -0400 Subject: [PATCH 10/17] [PM-5891] Add deepLinkGuard to /sm route to allow deep linking to Secrets Manager on login (#7686) --- bitwarden_license/bit-web/src/app/app-routing.module.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/bitwarden_license/bit-web/src/app/app-routing.module.ts b/bitwarden_license/bit-web/src/app/app-routing.module.ts index 0f10da154b9..f3f3c158708 100644 --- a/bitwarden_license/bit-web/src/app/app-routing.module.ts +++ b/bitwarden_license/bit-web/src/app/app-routing.module.ts @@ -13,6 +13,7 @@ const routes: Routes = [ }, { path: "sm", + canActivate: [deepLinkGuard()], loadChildren: async () => (await import("./secrets-manager/secrets-manager.module")).SecretsManagerModule, }, From c6327d7f12fd3aa21fbf3ced9e40644a8c015277 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:11:57 +0100 Subject: [PATCH 11/17] [PM-6780][PM-6781] Create vault-export-ui package / Migrate export-scope-callout.component to CL (#8318) * PM-6780 - Create vault-export-ui package * Migrate export-scope-callout to CL - Move export-scope-callout.component to vault-export-UI - Use bit-callout instead of app-callout - Make component standalone - Remove from jslib.module - Prefix selector with team-name - Export it from vault-export-ui * Update usage of tools-export-scope-callout for desktop * Update usage of tools-export-scope-callout for web * Update usage of tools-export-scope-callout for browser * Change package description --------- Co-authored-by: Daniel James Smith --- .eslintrc.json | 9 +++++++ apps/browser/src/popup/app.module.ts | 2 ++ .../popup/settings/export.component.html | 2 +- apps/browser/tsconfig.json | 1 + apps/desktop/src/app/app.module.ts | 2 ++ .../app/tools/export/export.component.html | 2 +- apps/desktop/tsconfig.json | 1 + .../vault-export/org-vault-export.module.ts | 9 ++++++- .../tools/vault-export/export.component.html | 4 +-- .../app/tools/vault-export/export.module.ts | 4 ++- apps/web/tsconfig.json | 1 + bitwarden_license/bit-web/tsconfig.json | 1 + libs/angular/src/jslib.module.ts | 3 --- libs/shared/tsconfig.libs.json | 1 + libs/tools/export/vault-export/README.md | 2 ++ .../vault-export-ui/jest.config.js | 13 ++++++++++ .../vault-export/vault-export-ui/package.json | 25 +++++++++++++++++++ .../export-scope-callout.component.html | 4 +-- .../export-scope-callout.component.ts | 7 +++++- .../vault-export/vault-export-ui/src/index.ts | 1 + .../vault-export-ui/tsconfig.json | 5 ++++ .../vault-export-ui/tsconfig.spec.json | 3 +++ tsconfig.eslint.json | 1 + tsconfig.json | 1 + 24 files changed, 92 insertions(+), 12 deletions(-) create mode 100644 libs/tools/export/vault-export/vault-export-ui/jest.config.js create mode 100644 libs/tools/export/vault-export/vault-export-ui/package.json rename libs/{angular/src/tools/export => tools/export/vault-export/vault-export-ui/src}/components/export-scope-callout.component.html (59%) rename libs/{angular/src/tools/export => tools/export/vault-export/vault-export-ui/src}/components/export-scope-callout.component.ts (86%) create mode 100644 libs/tools/export/vault-export/vault-export-ui/src/index.ts create mode 100644 libs/tools/export/vault-export/vault-export-ui/tsconfig.json create mode 100644 libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json diff --git a/.eslintrc.json b/.eslintrc.json index 47d45f23c3b..f21e2b08725 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -191,6 +191,15 @@ ] } }, + { + "files": ["libs/tools/export/vault-export/vault-export-ui/src/**/*.ts"], + "rules": { + "no-restricted-imports": [ + "error", + { "patterns": ["@bitwarden/vault-export-ui/*", "src/**/*"] } + ] + } + }, { "files": ["libs/importer/src/**/*.ts"], "rules": { diff --git a/apps/browser/src/popup/app.module.ts b/apps/browser/src/popup/app.module.ts index e438f2588b6..d179868448b 100644 --- a/apps/browser/src/popup/app.module.ts +++ b/apps/browser/src/popup/app.module.ts @@ -16,6 +16,7 @@ import { JslibModule } from "@bitwarden/angular/jslib.module"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { AvatarModule, ButtonModule } from "@bitwarden/components"; +import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccountSwitcherComponent } from "../auth/popup/account-switching/account-switcher.component"; import { AccountComponent } from "../auth/popup/account-switching/account.component"; @@ -107,6 +108,7 @@ import "../platform/popup/locales"; AvatarModule, AccountComponent, ButtonModule, + ExportScopeCalloutComponent, ], declarations: [ ActionButtonsComponent, diff --git a/apps/browser/src/tools/popup/settings/export.component.html b/apps/browser/src/tools/popup/settings/export.component.html index db072d6b504..aae3584f6c7 100644 --- a/apps/browser/src/tools/popup/settings/export.component.html +++ b/apps/browser/src/tools/popup/settings/export.component.html @@ -19,7 +19,7 @@ {{ "personalVaultExportPolicyInEffect" | i18n }} - +
diff --git a/apps/browser/tsconfig.json b/apps/browser/tsconfig.json index a4176be0b00..694246f59a1 100644 --- a/apps/browser/tsconfig.json +++ b/apps/browser/tsconfig.json @@ -20,6 +20,7 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/platform": ["../../libs/platform/src"], diff --git a/apps/desktop/src/app/app.module.ts b/apps/desktop/src/app/app.module.ts index 6317b6aaaf4..199ed510f1e 100644 --- a/apps/desktop/src/app/app.module.ts +++ b/apps/desktop/src/app/app.module.ts @@ -8,6 +8,7 @@ import { NgModule } from "@angular/core"; import { ColorPasswordCountPipe } from "@bitwarden/angular/pipes/color-password-count.pipe"; import { ColorPasswordPipe } from "@bitwarden/angular/pipes/color-password.pipe"; import { DialogModule } from "@bitwarden/components"; +import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; import { AccessibilityCookieComponent } from "../auth/accessibility-cookie.component"; import { DeleteAccountComponent } from "../auth/delete-account.component"; @@ -61,6 +62,7 @@ import { SendComponent } from "./tools/send/send.component"; DialogModule, DeleteAccountComponent, UserVerificationComponent, + ExportScopeCalloutComponent, ], declarations: [ AccessibilityCookieComponent, diff --git a/apps/desktop/src/app/tools/export/export.component.html b/apps/desktop/src/app/tools/export/export.component.html index 8b3af4f1da3..a6ed0981811 100644 --- a/apps/desktop/src/app/tools/export/export.component.html +++ b/apps/desktop/src/app/tools/export/export.component.html @@ -9,7 +9,7 @@ > {{ "personalVaultExportPolicyInEffect" | i18n }} - +

{{ "exportVault" | i18n }} diff --git a/apps/desktop/tsconfig.json b/apps/desktop/tsconfig.json index 63a1af06d78..a62d494f294 100644 --- a/apps/desktop/tsconfig.json +++ b/apps/desktop/tsconfig.json @@ -20,6 +20,7 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/node/*": ["../../libs/node/src/*"], diff --git a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts index 34b4fa4adba..ca8a75165b5 100644 --- a/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts +++ b/apps/web/src/app/admin-console/organizations/tools/vault-export/org-vault-export.module.ts @@ -1,12 +1,19 @@ import { NgModule } from "@angular/core"; +import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; + import { LooseComponentsModule, SharedModule } from "../../../../shared"; import { OrganizationVaultExportRoutingModule } from "./org-vault-export-routing.module"; import { OrganizationVaultExportComponent } from "./org-vault-export.component"; @NgModule({ - imports: [SharedModule, LooseComponentsModule, OrganizationVaultExportRoutingModule], + imports: [ + SharedModule, + LooseComponentsModule, + OrganizationVaultExportRoutingModule, + ExportScopeCalloutComponent, + ], declarations: [OrganizationVaultExportComponent], }) export class OrganizationVaultExportModule {} diff --git a/apps/web/src/app/tools/vault-export/export.component.html b/apps/web/src/app/tools/vault-export/export.component.html index 8ed82b9fd9e..9f47adf8aa5 100644 --- a/apps/web/src/app/tools/vault-export/export.component.html +++ b/apps/web/src/app/tools/vault-export/export.component.html @@ -5,10 +5,10 @@ {{ "personalVaultExportPolicyInEffect" | i18n }} - + > diff --git a/apps/web/src/app/tools/vault-export/export.module.ts b/apps/web/src/app/tools/vault-export/export.module.ts index aca8bdbd4ec..ddf82b0a100 100644 --- a/apps/web/src/app/tools/vault-export/export.module.ts +++ b/apps/web/src/app/tools/vault-export/export.module.ts @@ -1,12 +1,14 @@ import { NgModule } from "@angular/core"; +import { ExportScopeCalloutComponent } from "@bitwarden/vault-export-ui"; + import { LooseComponentsModule, SharedModule } from "../../shared"; import { ExportRoutingModule } from "./export-routing.module"; import { ExportComponent } from "./export.component"; @NgModule({ - imports: [SharedModule, LooseComponentsModule, ExportRoutingModule], + imports: [SharedModule, LooseComponentsModule, ExportRoutingModule, ExportScopeCalloutComponent], declarations: [ExportComponent], }) export class ExportModule {} diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index ba8060b93af..543d7f25b12 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -15,6 +15,7 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../../libs/importer/src"], "@bitwarden/importer/ui": ["../../libs/importer/src/components"], "@bitwarden/platform": ["../../libs/platform/src"], diff --git a/bitwarden_license/bit-web/tsconfig.json b/bitwarden_license/bit-web/tsconfig.json index 01610c5e4ac..0f19c6736a3 100644 --- a/bitwarden_license/bit-web/tsconfig.json +++ b/bitwarden_license/bit-web/tsconfig.json @@ -11,6 +11,7 @@ "@bitwarden/vault-export-core": [ "../../libs/tools/export/vault-export/vault-export-core/src" ], + "@bitwarden/vault-export-ui": ["../../libs/tools/export/vault-export/vault-export-core/src"], "@bitwarden/platform": ["../../libs/platform/src"], "@bitwarden/vault": ["../../libs/vault/src"], "@bitwarden/web-vault/*": ["../../apps/web/src/*"] diff --git a/libs/angular/src/jslib.module.ts b/libs/angular/src/jslib.module.ts index 11771067674..64fb44e3b8b 100644 --- a/libs/angular/src/jslib.module.ts +++ b/libs/angular/src/jslib.module.ts @@ -29,7 +29,6 @@ import { UserTypePipe } from "./pipes/user-type.pipe"; import { EllipsisPipe } from "./platform/pipes/ellipsis.pipe"; import { FingerprintPipe } from "./platform/pipes/fingerprint.pipe"; import { I18nPipe } from "./platform/pipes/i18n.pipe"; -import { ExportScopeCalloutComponent } from "./tools/export/components/export-scope-callout.component"; import { PasswordStrengthComponent } from "./tools/password-strength/password-strength.component"; import { IconComponent } from "./vault/components/icon.component"; @@ -54,7 +53,6 @@ import { IconComponent } from "./vault/components/icon.component"; CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, - ExportScopeCalloutComponent, FallbackSrcDirective, I18nPipe, IconComponent, @@ -85,7 +83,6 @@ import { IconComponent } from "./vault/components/icon.component"; CopyTextDirective, CreditCardNumberPipe, EllipsisPipe, - ExportScopeCalloutComponent, FallbackSrcDirective, I18nPipe, IconComponent, diff --git a/libs/shared/tsconfig.libs.json b/libs/shared/tsconfig.libs.json index 079a49fbd5d..713d34a10e4 100644 --- a/libs/shared/tsconfig.libs.json +++ b/libs/shared/tsconfig.libs.json @@ -10,6 +10,7 @@ "@bitwarden/common/*": ["../common/src/*"], "@bitwarden/components": ["../components/src"], "@bitwarden/vault-export-core": ["../tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["../tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["../importer/src"], "@bitwarden/importer/ui": ["../importer/src/components"], "@bitwarden/platform": ["../platform/src"], diff --git a/libs/tools/export/vault-export/README.md b/libs/tools/export/vault-export/README.md index 45d9e08e5e9..e6bce1c525f 100644 --- a/libs/tools/export/vault-export/README.md +++ b/libs/tools/export/vault-export/README.md @@ -13,3 +13,5 @@ Currently in use by the Bitwarden Web Vault, CLI, desktop app and browser extens ## vault-export-ui Package name: `@bitwarden/vault-export-ui` + +Contains all UI components used for the vault-export diff --git a/libs/tools/export/vault-export/vault-export-ui/jest.config.js b/libs/tools/export/vault-export/vault-export-ui/jest.config.js new file mode 100644 index 00000000000..955b8e7763c --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/jest.config.js @@ -0,0 +1,13 @@ +const { pathsToModuleNameMapper } = require("ts-jest"); + +const { compilerOptions } = require("../../../../shared/tsconfig.libs"); + +/** @type {import('jest').Config} */ +module.exports = { + testMatch: ["**/+(*.)+(spec).+(ts)"], + preset: "ts-jest", + testEnvironment: "jsdom", + moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { + prefix: "/../../../", + }), +}; diff --git a/libs/tools/export/vault-export/vault-export-ui/package.json b/libs/tools/export/vault-export/vault-export-ui/package.json new file mode 100644 index 00000000000..e27140f3657 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/package.json @@ -0,0 +1,25 @@ +{ + "name": "@bitwarden/vault-export-ui", + "version": "0.0.0", + "description": "Angular components for the Bitwarden vault exporter", + "keywords": [ + "bitwarden" + ], + "author": "Bitwarden Inc.", + "homepage": "https://bitwarden.com", + "repository": { + "type": "git", + "url": "https://github.com/bitwarden/clients" + }, + "license": "GPL-3.0", + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && tsc", + "build:watch": "npm run clean && tsc -watch" + }, + "dependencies": { + "@bitwarden/common": "file:../../../../common", + "@bitwarden/angular": "file:../../../../angular", + "@bitwarden/vault-export-core": "file:../vault-export-core" + } +} diff --git a/libs/angular/src/tools/export/components/export-scope-callout.component.html b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html similarity index 59% rename from libs/angular/src/tools/export/components/export-scope-callout.component.html rename to libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html index c6b5e1e8523..a660219499f 100644 --- a/libs/angular/src/tools/export/components/export-scope-callout.component.html +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.html @@ -1,5 +1,5 @@ - + {{ scopeConfig.description | i18n: scopeConfig.scopeIdentifier }} - + diff --git a/libs/angular/src/tools/export/components/export-scope-callout.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts similarity index 86% rename from libs/angular/src/tools/export/components/export-scope-callout.component.ts rename to libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts index 545dfe4560a..0f246c3a341 100644 --- a/libs/angular/src/tools/export/components/export-scope-callout.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export-scope-callout.component.ts @@ -1,11 +1,16 @@ +import { CommonModule } from "@angular/common"; import { Component, Input, OnInit } from "@angular/core"; +import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { CalloutModule } from "@bitwarden/components"; @Component({ - selector: "app-export-scope-callout", + selector: "tools-export-scope-callout", templateUrl: "export-scope-callout.component.html", + standalone: true, + imports: [CommonModule, JslibModule, CalloutModule], }) export class ExportScopeCalloutComponent implements OnInit { show = false; diff --git a/libs/tools/export/vault-export/vault-export-ui/src/index.ts b/libs/tools/export/vault-export/vault-export-ui/src/index.ts new file mode 100644 index 00000000000..4165ee4558a --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/src/index.ts @@ -0,0 +1 @@ +export { ExportScopeCalloutComponent } from "./components/export-scope-callout.component"; diff --git a/libs/tools/export/vault-export/vault-export-ui/tsconfig.json b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json new file mode 100644 index 00000000000..5cb90260371 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../../../shared/tsconfig.libs", + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json b/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json new file mode 100644 index 00000000000..fc8520e7376 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-ui/tsconfig.spec.json @@ -0,0 +1,3 @@ +{ + "extends": "./tsconfig.json" +} diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json index c8144b97b9f..19d35b28969 100644 --- a/tsconfig.eslint.json +++ b/tsconfig.eslint.json @@ -22,6 +22,7 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/vault-export-core": [".libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": [".libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/platform": ["./libs/platform/src"], diff --git a/tsconfig.json b/tsconfig.json index 4aaf670dbc2..ab3f8861a9a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -23,6 +23,7 @@ "@bitwarden/common/*": ["./libs/common/src/*"], "@bitwarden/components": ["./libs/components/src"], "@bitwarden/vault-export-core": ["./libs/tools/export/vault-export/vault-export-core/src"], + "@bitwarden/vault-export-ui": ["./libs/tools/export/vault-export/vault-export-ui/src"], "@bitwarden/importer/core": ["./libs/importer/src"], "@bitwarden/importer/ui": ["./libs/importer/src/components"], "@bitwarden/platform": ["./libs/platform/src"], From e2fe1e1567994e376d82845de09310b612b16579 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 20 Mar 2024 23:39:25 +0100 Subject: [PATCH 12/17] Replace app-callout with bit-callout and fixed the used types forthe styles (#8414) Co-authored-by: Daniel James Smith --- .../user-verification/user-verification-dialog.component.html | 4 ++-- .../user-verification/user-verification-dialog.component.ts | 2 ++ .../user-verification/user-verification-dialog.types.ts | 4 ++-- .../user-verification-form-input.component.html | 4 ++-- .../user-verification-form-input.component.ts | 2 ++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.html b/libs/auth/src/angular/user-verification/user-verification-dialog.component.html index 66e006d9453..aa4d26ae61f 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.html +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.html @@ -18,12 +18,12 @@ {{ dialogOptions.bodyText | i18n }}

- {{ dialogOptions.calloutOptions.text | i18n }} - +
diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts index d3a89473525..7b2c869e3af 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.component.ts @@ -12,6 +12,7 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { AsyncActionsModule, ButtonModule, + CalloutModule, DialogModule, DialogService, } from "@bitwarden/components"; @@ -34,6 +35,7 @@ import { UserVerificationFormInputComponent } from "./user-verification-form-inp DialogModule, AsyncActionsModule, UserVerificationFormInputComponent, + CalloutModule, ], }) export class UserVerificationDialogComponent { diff --git a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts index 2480162f867..f4637c770a2 100644 --- a/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts +++ b/libs/auth/src/angular/user-verification/user-verification-dialog.types.ts @@ -11,9 +11,9 @@ export type UserVerificationCalloutOptions = { /** * The type of the callout. - * Can be "warning", "danger", "error", or "tip". + * Can be "warning", "danger", "info", or "success". */ - type: "warning" | "danger" | "error" | "tip"; + type: "warning" | "danger" | "info" | "success"; }; /** diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html index afc99b32cb7..f532a3b23fd 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.html +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.html @@ -49,12 +49,12 @@

- + {{ "couldNotCompleteBiometrics" | i18n }} - + diff --git a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts index 4f216a76e8c..8cb40d94524 100644 --- a/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts +++ b/libs/auth/src/angular/user-verification/user-verification-form-input.component.ts @@ -20,6 +20,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { AsyncActionsModule, ButtonModule, + CalloutModule, FormFieldModule, IconButtonModule, IconModule, @@ -62,6 +63,7 @@ import { ActiveClientVerificationOption } from "./active-client-verification-opt IconModule, LinkModule, ButtonModule, + CalloutModule, ], }) // eslint-disable-next-line rxjs-angular/prefer-takeuntil From 2111b37c329cb21ba5ad2ce231e5134129d75d5e Mon Sep 17 00:00:00 2001 From: Jake Fink Date: Wed, 20 Mar 2024 20:33:57 -0400 Subject: [PATCH 13/17] [PM-5404, PM-3518] Migrate user decryption options to new service (#7344) * create new user decryption options service * rename new service to user decryption options * add hasMasterPassword to user decryption options service * migrate device trust service to new user decryption options service * add migration for user-decryption-options * migrate sync service and calls to trust-device-service * rename abstraction file * migrate two factor component * migrate two factor spec * migrate sso component * migrate set-password component * migrate base login decryption component * migrate organization options component * fix component imports * add missing imports - remove state service calls - add update user decryption options method * remove acct decryption options from account * lint * fix tests and linting * fix browser * fix desktop * add user decryption options service to cli * remove default value from migration * bump migration number * fix merge conflict * fix vault timeout settings * fix cli * more fixes * add user decryption options service to deps of vault timeout settings service * update login strategy service with user decryption options * remove early return from sync bandaid for user decryption options * move user decryption options service to lib/auth * move user decryption options to libs/auth * fix reference * fix browser * check user decryption options after 2fa check * update migration and revert tsconfig changes * add more documentation * clear user decryption options on logout * fix tests by creating helper for user decryption options * fix tests * pr feedback * fix factory * update migration * add tests * update missed migration num in test --- .../service-factories/auth-service.factory.ts | 2 +- .../device-trust-crypto-service.factory.ts | 9 +- .../login-strategy-service.factory.ts | 14 +- ...user-decryption-options-service.factory.ts | 46 ++++ .../user-verification-service.factory.ts | 6 + .../src/auth/popup/set-password.component.ts | 3 + apps/browser/src/auth/popup/sso.component.ts | 7 +- .../src/auth/popup/two-factor.component.ts | 7 +- .../browser/src/background/main.background.ts | 10 + .../vault-timeout-settings-service.factory.ts | 6 + apps/cli/src/bw.ts | 10 + .../src/auth/set-password.component.ts | 3 + apps/desktop/src/auth/sso.component.ts | 7 +- apps/desktop/src/auth/two-factor.component.ts | 7 +- apps/web/src/app/auth/sso.component.ts | 7 +- apps/web/src/app/auth/two-factor.component.ts | 7 +- .../organization-options.component.ts | 6 +- ...base-login-decryption-options.component.ts | 26 +- .../auth/components/set-password.component.ts | 15 +- .../src/auth/components/sso.component.spec.ts | 92 +++---- .../src/auth/components/sso.component.ts | 34 ++- .../components/two-factor.component.spec.ts | 91 ++++--- .../auth/components/two-factor.component.ts | 27 +- libs/angular/src/auth/guards/lock.guard.ts | 2 +- .../angular/src/auth/guards/redirect.guard.ts | 2 +- .../guards/tde-decryption-required.guard.ts | 2 +- .../src/services/jslib-services.module.ts | 19 +- libs/auth/src/common/abstractions/index.ts | 1 + ...-decryption-options.service.abstraction.ts | 34 +++ .../auth-request-login.strategy.spec.ts | 4 + .../auth-request-login.strategy.ts | 3 + .../login-strategies/login.strategy.spec.ts | 16 +- .../common/login-strategies/login.strategy.ts | 9 +- .../password-login.strategy.spec.ts | 4 + .../password-login.strategy.ts | 3 + .../sso-login.strategy.spec.ts | 8 +- .../login-strategies/sso-login.strategy.ts | 7 +- .../user-api-login.strategy.spec.ts | 4 + .../user-api-login.strategy.ts | 3 + .../webauthn-login.strategy.spec.ts | 4 + .../webauthn-login.strategy.ts | 3 + libs/auth/src/common/models/domain/index.ts | 1 + .../models/domain/user-decryption-options.ts | 153 +++++++++++ libs/auth/src/common/models/index.ts | 1 + .../spec/fake-user-decryption-options.ts | 38 +++ libs/auth/src/common/models/spec/index.ts | 1 + libs/auth/src/common/services/index.ts | 1 + .../login-strategy.service.spec.ts | 9 +- .../login-strategy.service.ts | 7 + .../user-decryption-options.service.spec.ts | 94 +++++++ .../user-decryption-options.service.ts | 47 ++++ ...device-trust-crypto.service.abstraction.ts | 5 +- .../src/auth/models/domain/auth-result.ts | 2 +- .../key-connector-user-decryption-option.ts | 3 - .../trusted-device-user-decryption-option.ts | 7 - ...ice-trust-crypto.service.implementation.ts | 18 +- .../device-trust-crypto.service.spec.ts | 12 +- .../user-verification.service.ts | 21 +- .../platform/abstractions/state.service.ts | 9 +- .../src/platform/models/domain/account.ts | 99 -------- .../src/platform/services/state.service.ts | 38 +-- .../src/platform/state/state-definitions.ts | 1 + .../vault-timeout-settings.service.spec.ts | 39 ++- .../vault-timeout-settings.service.ts | 16 +- libs/common/src/state-migrations/migrate.ts | 6 +- ...cryption-options-to-state-provider.spec.ts | 238 ++++++++++++++++++ ...er-decryption-options-to-state-provider.ts | 57 +++++ .../src/vault/services/sync/sync.service.ts | 25 +- 68 files changed, 1158 insertions(+), 360 deletions(-) create mode 100644 apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts create mode 100644 libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts create mode 100644 libs/auth/src/common/models/domain/user-decryption-options.ts create mode 100644 libs/auth/src/common/models/spec/fake-user-decryption-options.ts create mode 100644 libs/auth/src/common/models/spec/index.ts create mode 100644 libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts create mode 100644 libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts delete mode 100644 libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts delete mode 100644 libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts create mode 100644 libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts create mode 100644 libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts diff --git a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts index d0a4d2db1b5..fa52ca6231c 100644 --- a/apps/browser/src/auth/background/service-factories/auth-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/auth-service.factory.ts @@ -15,8 +15,8 @@ import { factory, } from "../../../platform/background/service-factories/factory-options"; import { - messagingServiceFactory, MessagingServiceInitOptions, + messagingServiceFactory, } from "../../../platform/background/service-factories/messaging-service.factory"; import { StateServiceInitOptions, diff --git a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts index 6b8d3c09e33..5916f38441f 100644 --- a/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/device-trust-crypto-service.factory.ts @@ -43,6 +43,11 @@ import { stateServiceFactory, } from "../../../platform/background/service-factories/state-service.factory"; +import { + UserDecryptionOptionsServiceInitOptions, + userDecryptionOptionsServiceFactory, +} from "./user-decryption-options-service.factory"; + type DeviceTrustCryptoServiceFactoryOptions = FactoryOptions; export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactoryOptions & @@ -54,7 +59,8 @@ export type DeviceTrustCryptoServiceInitOptions = DeviceTrustCryptoServiceFactor AppIdServiceInitOptions & DevicesApiServiceInitOptions & I18nServiceInitOptions & - PlatformUtilsServiceInitOptions; + PlatformUtilsServiceInitOptions & + UserDecryptionOptionsServiceInitOptions; export function deviceTrustCryptoServiceFactory( cache: { deviceTrustCryptoService?: DeviceTrustCryptoServiceAbstraction } & CachedServices, @@ -75,6 +81,7 @@ export function deviceTrustCryptoServiceFactory( await devicesApiServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await platformUtilsServiceFactory(cache, opts), + await userDecryptionOptionsServiceFactory(cache, opts), ), ); } diff --git a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts index b0ae87a75f7..2cc4692ca9c 100644 --- a/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/login-strategy-service.factory.ts @@ -9,7 +9,10 @@ import { ApiServiceInitOptions, } from "../../../platform/background/service-factories/api-service.factory"; import { appIdServiceFactory } from "../../../platform/background/service-factories/app-id-service.factory"; -import { billingAccountProfileStateServiceFactory } from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; +import { + billingAccountProfileStateServiceFactory, + BillingAccountProfileStateServiceInitOptions, +} from "../../../platform/background/service-factories/billing-account-profile-state-service.factory"; import { CryptoServiceInitOptions, cryptoServiceFactory, @@ -70,6 +73,10 @@ import { } from "./key-connector-service.factory"; import { tokenServiceFactory, TokenServiceInitOptions } from "./token-service.factory"; import { twoFactorServiceFactory, TwoFactorServiceInitOptions } from "./two-factor-service.factory"; +import { + internalUserDecryptionOptionServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "./user-decryption-options-service.factory"; type LoginStrategyServiceFactoryOptions = FactoryOptions; @@ -90,7 +97,9 @@ export type LoginStrategyServiceInitOptions = LoginStrategyServiceFactoryOptions PasswordStrengthServiceInitOptions & DeviceTrustCryptoServiceInitOptions & AuthRequestServiceInitOptions & - GlobalStateProviderInitOptions; + UserDecryptionOptionsServiceInitOptions & + GlobalStateProviderInitOptions & + BillingAccountProfileStateServiceInitOptions; export function loginStrategyServiceFactory( cache: { loginStrategyService?: LoginStrategyServiceAbstraction } & CachedServices, @@ -119,6 +128,7 @@ export function loginStrategyServiceFactory( await policyServiceFactory(cache, opts), await deviceTrustCryptoServiceFactory(cache, opts), await authRequestServiceFactory(cache, opts), + await internalUserDecryptionOptionServiceFactory(cache, opts), await globalStateProviderFactory(cache, opts), await billingAccountProfileStateServiceFactory(cache, opts), ), diff --git a/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts new file mode 100644 index 00000000000..549639a3c78 --- /dev/null +++ b/apps/browser/src/auth/background/service-factories/user-decryption-options-service.factory.ts @@ -0,0 +1,46 @@ +import { + InternalUserDecryptionOptionsServiceAbstraction, + UserDecryptionOptionsService, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; + +import { + CachedServices, + factory, + FactoryOptions, +} from "../../../platform/background/service-factories/factory-options"; +import { + stateProviderFactory, + StateProviderInitOptions, +} from "../../../platform/background/service-factories/state-provider.factory"; + +type UserDecryptionOptionsServiceFactoryOptions = FactoryOptions; + +export type UserDecryptionOptionsServiceInitOptions = UserDecryptionOptionsServiceFactoryOptions & + StateProviderInitOptions; + +export function userDecryptionOptionsServiceFactory( + cache: { + userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction; + } & CachedServices, + opts: UserDecryptionOptionsServiceInitOptions, +): Promise { + return factory( + cache, + "userDecryptionOptionsService", + opts, + async () => new UserDecryptionOptionsService(await stateProviderFactory(cache, opts)), + ); +} + +export async function internalUserDecryptionOptionServiceFactory( + cache: { + userDecryptionOptionsService?: InternalUserDecryptionOptionsServiceAbstraction; + } & CachedServices, + opts: UserDecryptionOptionsServiceInitOptions, +): Promise { + return (await userDecryptionOptionsServiceFactory( + cache, + opts, + )) as InternalUserDecryptionOptionsServiceAbstraction; +} diff --git a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts index ff08ddf689f..e8be9099cac 100644 --- a/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts +++ b/apps/browser/src/auth/background/service-factories/user-verification-service.factory.ts @@ -32,6 +32,10 @@ import { } from "../../../platform/background/service-factories/state-service.factory"; import { PinCryptoServiceInitOptions, pinCryptoServiceFactory } from "./pin-crypto-service.factory"; +import { + userDecryptionOptionsServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "./user-decryption-options-service.factory"; import { UserVerificationApiServiceInitOptions, userVerificationApiServiceFactory, @@ -44,6 +48,7 @@ export type UserVerificationServiceInitOptions = UserVerificationServiceFactoryO CryptoServiceInitOptions & I18nServiceInitOptions & UserVerificationApiServiceInitOptions & + UserDecryptionOptionsServiceInitOptions & PinCryptoServiceInitOptions & LogServiceInitOptions & VaultTimeoutSettingsServiceInitOptions & @@ -63,6 +68,7 @@ export function userVerificationServiceFactory( await cryptoServiceFactory(cache, opts), await i18nServiceFactory(cache, opts), await userVerificationApiServiceFactory(cache, opts), + await userDecryptionOptionsServiceFactory(cache, opts), await pinCryptoServiceFactory(cache, opts), await logServiceFactory(cache, opts), await vaultTimeoutSettingsServiceFactory(cache, opts), diff --git a/apps/browser/src/auth/popup/set-password.component.ts b/apps/browser/src/auth/popup/set-password.component.ts index ac98966b4ab..ea1cacc7ac5 100644 --- a/apps/browser/src/auth/popup/set-password.component.ts +++ b/apps/browser/src/auth/popup/set-password.component.ts @@ -2,6 +2,7 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -37,6 +38,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent { route: ActivatedRoute, organizationApiService: OrganizationApiServiceAbstraction, organizationUserService: OrganizationUserService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, ) { @@ -55,6 +57,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent { stateService, organizationApiService, organizationUserService, + userDecryptionOptionsService, ssoLoginService, dialogService, ); diff --git a/apps/browser/src/auth/popup/sso.component.ts b/apps/browser/src/auth/popup/sso.component.ts index 19d7977819f..7b61a04bfda 100644 --- a/apps/browser/src/auth/popup/sso.component.ts +++ b/apps/browser/src/auth/popup/sso.component.ts @@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -39,6 +42,7 @@ export class SsoComponent extends BaseSsoComponent { syncService: SyncService, environmentService: EnvironmentService, logService: LogService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, protected authService: AuthService, @Inject(WINDOW) private win: Window, @@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService, passwordGenerationService, logService, + userDecryptionOptionsService, configService, ); diff --git a/apps/browser/src/auth/popup/two-factor.component.ts b/apps/browser/src/auth/popup/two-factor.component.ts index e511122f9a5..0a950d6c1b5 100644 --- a/apps/browser/src/auth/popup/two-factor.component.ts +++ b/apps/browser/src/auth/popup/two-factor.component.ts @@ -5,7 +5,10 @@ import { filter, first, takeUntil } from "rxjs/operators"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -55,6 +58,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService: TwoFactorService, appIdService: AppIdService, loginService: LoginService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, private dialogService: DialogService, @@ -75,6 +79,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService, appIdService, loginService, + userDecryptionOptionsService, ssoLoginService, configService, ); diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index b3b9be3dcd4..b34f07f657f 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -5,6 +5,8 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + InternalUserDecryptionOptionsServiceAbstraction, + UserDecryptionOptionsService, AuthRequestServiceAbstraction, AuthRequestService, } from "@bitwarden/auth/common"; @@ -242,6 +244,7 @@ export default class MainBackground { environmentService: BrowserEnvironmentService; cipherService: CipherServiceAbstraction; folderService: InternalFolderServiceAbstraction; + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; collectionService: CollectionServiceAbstraction; vaultTimeoutService: VaultTimeoutService; vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction; @@ -539,6 +542,8 @@ export default class MainBackground { }; })(); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.keyGenerationService, @@ -550,6 +555,7 @@ export default class MainBackground { this.devicesApiService, this.i18nService, this.platformUtilsService, + this.userDecryptionOptionsService, ); this.devicesService = new DevicesServiceImplementation(this.devicesApiService); @@ -590,6 +596,7 @@ export default class MainBackground { this.policyService, this.deviceTrustCryptoService, this.authRequestService, + this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, ); @@ -631,6 +638,7 @@ export default class MainBackground { this.folderApiService = new FolderApiService(this.folderService, this.apiService); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.userDecryptionOptionsService, this.cryptoService, this.tokenService, this.policyService, @@ -650,6 +658,7 @@ export default class MainBackground { this.cryptoService, this.i18nService, this.userVerificationApiService, + this.userDecryptionOptionsService, this.pinCryptoService, this.logService, this.vaultTimeoutSettingsService, @@ -717,6 +726,7 @@ export default class MainBackground { this.folderApiService, this.organizationService, this.sendApiService, + this.userDecryptionOptionsService, this.avatarService, logoutCallback, this.billingAccountProfileStateService, diff --git a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts index febc605bc8a..92a1d83dd25 100644 --- a/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts +++ b/apps/browser/src/background/service-factories/vault-timeout-settings-service.factory.ts @@ -9,6 +9,10 @@ import { tokenServiceFactory, TokenServiceInitOptions, } from "../../auth/background/service-factories/token-service.factory"; +import { + userDecryptionOptionsServiceFactory, + UserDecryptionOptionsServiceInitOptions, +} from "../../auth/background/service-factories/user-decryption-options-service.factory"; import { biometricStateServiceFactory, BiometricStateServiceInitOptions, @@ -30,6 +34,7 @@ import { type VaultTimeoutSettingsServiceFactoryOptions = FactoryOptions; export type VaultTimeoutSettingsServiceInitOptions = VaultTimeoutSettingsServiceFactoryOptions & + UserDecryptionOptionsServiceInitOptions & CryptoServiceInitOptions & TokenServiceInitOptions & PolicyServiceInitOptions & @@ -46,6 +51,7 @@ export function vaultTimeoutSettingsServiceFactory( opts, async () => new VaultTimeoutSettingsService( + await userDecryptionOptionsServiceFactory(cache, opts), await cryptoServiceFactory(cache, opts), await tokenServiceFactory(cache, opts), await policyServiceFactory(cache, opts), diff --git a/apps/cli/src/bw.ts b/apps/cli/src/bw.ts index 95e306bfc0e..55bc46e41e5 100644 --- a/apps/cli/src/bw.ts +++ b/apps/cli/src/bw.ts @@ -5,11 +5,13 @@ import { program } from "commander"; import * as jsdom from "jsdom"; import { + InternalUserDecryptionOptionsServiceAbstraction, AuthRequestService, LoginStrategyService, LoginStrategyServiceAbstraction, PinCryptoService, PinCryptoServiceAbstraction, + UserDecryptionOptionsService, } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventUploadService as EventUploadServiceAbstraction } from "@bitwarden/common/abstractions/event/event-upload.service"; @@ -169,6 +171,7 @@ export class Main { eventUploadService: EventUploadServiceAbstraction; passwordGenerationService: PasswordGenerationServiceAbstraction; passwordStrengthService: PasswordStrengthServiceAbstraction; + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction; totpService: TotpService; containerService: ContainerService; auditService: AuditService; @@ -436,6 +439,8 @@ export class Main { this.stateService, ); + this.userDecryptionOptionsService = new UserDecryptionOptionsService(this.stateProvider); + this.devicesApiService = new DevicesApiServiceImplementation(this.apiService); this.deviceTrustCryptoService = new DeviceTrustCryptoService( this.keyGenerationService, @@ -447,6 +452,7 @@ export class Main { this.devicesApiService, this.i18nService, this.platformUtilsService, + this.userDecryptionOptionsService, ); this.authRequestService = new AuthRequestService( @@ -478,6 +484,7 @@ export class Main { this.policyService, this.deviceTrustCryptoService, this.authRequestService, + this.userDecryptionOptionsService, this.globalStateProvider, this.billingAccountProfileStateService, ); @@ -529,6 +536,7 @@ export class Main { this.biometricStateService = new DefaultBiometricStateService(this.stateProvider); this.vaultTimeoutSettingsService = new VaultTimeoutSettingsService( + this.userDecryptionOptionsService, this.cryptoService, this.tokenService, this.policyService, @@ -548,6 +556,7 @@ export class Main { this.cryptoService, this.i18nService, this.userVerificationApiService, + this.userDecryptionOptionsService, this.pinCryptoService, this.logService, this.vaultTimeoutSettingsService, @@ -589,6 +598,7 @@ export class Main { this.folderApiService, this.organizationService, this.sendApiService, + this.userDecryptionOptionsService, this.avatarService, async (expired: boolean) => await this.logout(), this.billingAccountProfileStateService, diff --git a/apps/desktop/src/auth/set-password.component.ts b/apps/desktop/src/auth/set-password.component.ts index 9505cf6aa19..a75668a8567 100644 --- a/apps/desktop/src/auth/set-password.component.ts +++ b/apps/desktop/src/auth/set-password.component.ts @@ -2,6 +2,7 @@ import { Component, NgZone, OnDestroy } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SetPasswordComponent as BaseSetPasswordComponent } from "@bitwarden/angular/auth/components/set-password.component"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -44,6 +45,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On stateService: StateService, organizationApiService: OrganizationApiServiceAbstraction, organizationUserService: OrganizationUserService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, ) { @@ -62,6 +64,7 @@ export class SetPasswordComponent extends BaseSetPasswordComponent implements On stateService, organizationApiService, organizationUserService, + userDecryptionOptionsService, ssoLoginService, dialogService, ); diff --git a/apps/desktop/src/auth/sso.component.ts b/apps/desktop/src/auth/sso.component.ts index 123961482a9..0268133192f 100644 --- a/apps/desktop/src/auth/sso.component.ts +++ b/apps/desktop/src/auth/sso.component.ts @@ -2,7 +2,10 @@ import { Component } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; @@ -34,6 +37,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService: EnvironmentService, passwordGenerationService: PasswordGenerationServiceAbstraction, logService: LogService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, ) { super( @@ -49,6 +53,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService, passwordGenerationService, logService, + userDecryptionOptionsService, configService, ); super.onSuccessfulLogin = async () => { diff --git a/apps/desktop/src/auth/two-factor.component.ts b/apps/desktop/src/auth/two-factor.component.ts index 7c624f4adb9..d9c30f65953 100644 --- a/apps/desktop/src/auth/two-factor.component.ts +++ b/apps/desktop/src/auth/two-factor.component.ts @@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -53,6 +56,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService: TwoFactorService, appIdService: AppIdService, loginService: LoginService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigServiceAbstraction, @Inject(WINDOW) protected win: Window, @@ -71,6 +75,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent { twoFactorService, appIdService, loginService, + userDecryptionOptionsService, ssoLoginService, configService, ); diff --git a/apps/web/src/app/auth/sso.component.ts b/apps/web/src/app/auth/sso.component.ts index 9363cb3366e..2ef4f3eb155 100644 --- a/apps/web/src/app/auth/sso.component.ts +++ b/apps/web/src/app/auth/sso.component.ts @@ -3,7 +3,10 @@ import { ActivatedRoute, Router } from "@angular/router"; import { first } from "rxjs/operators"; import { SsoComponent as BaseSsoComponent } from "@bitwarden/angular/auth/components/sso.component"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrgDomainApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization-domain/org-domain-api.service.abstraction"; import { OrganizationDomainSsoDetailsResponse } from "@bitwarden/common/admin-console/abstractions/organization-domain/responses/organization-domain-sso-details.response"; @@ -41,6 +44,7 @@ export class SsoComponent extends BaseSsoComponent { logService: LogService, private orgDomainApiService: OrgDomainApiServiceAbstraction, private validationService: ValidationService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, configService: ConfigServiceAbstraction, ) { super( @@ -56,6 +60,7 @@ export class SsoComponent extends BaseSsoComponent { environmentService, passwordGenerationService, logService, + userDecryptionOptionsService, configService, ); this.redirectUri = window.location.origin + "/sso-connector.html"; diff --git a/apps/web/src/app/auth/two-factor.component.ts b/apps/web/src/app/auth/two-factor.component.ts index 44a9674fbdf..06b76b6b546 100644 --- a/apps/web/src/app/auth/two-factor.component.ts +++ b/apps/web/src/app/auth/two-factor.component.ts @@ -4,7 +4,10 @@ import { ActivatedRoute, Router } from "@angular/router"; import { TwoFactorComponent as BaseTwoFactorComponent } from "@bitwarden/angular/auth/components/two-factor.component"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; import { ModalService } from "@bitwarden/angular/services/modal.service"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -44,6 +47,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest twoFactorService: TwoFactorService, appIdService: AppIdService, loginService: LoginService, + userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, ssoLoginService: SsoLoginServiceAbstraction, configService: ConfigServiceAbstraction, @Inject(WINDOW) protected win: Window, @@ -62,6 +66,7 @@ export class TwoFactorComponent extends BaseTwoFactorComponent implements OnDest twoFactorService, appIdService, loginService, + userDecryptionOptionsService, ssoLoginService, configService, ); diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts index b7cb33d97ee..fa81abdc54f 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/components/organization-options.component.ts @@ -1,6 +1,7 @@ import { Component, Inject, OnDestroy, OnInit } from "@angular/core"; import { combineLatest, map, Observable, Subject, takeUntil } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -12,7 +13,6 @@ import { Policy } from "@bitwarden/common/admin-console/models/domain/policy"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; -import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { DialogService } from "@bitwarden/components"; @@ -44,8 +44,8 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { private logService: LogService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private dialogService: DialogService, - private stateService: StateService, ) {} async ngOnInit() { @@ -56,7 +56,7 @@ export class OrganizationOptionsComponent implements OnInit, OnDestroy { combineLatest([ this.organization$, resetPasswordPolicies$, - this.stateService.getAccountDecryptionOptions(), + this.userDecryptionOptionsService.userDecryptionOptions$, ]) .pipe(takeUntil(this.destroy$)) .subscribe(([organization, resetPasswordPolicies, decryptionOptions]) => { diff --git a/libs/angular/src/auth/components/base-login-decryption-options.component.ts b/libs/angular/src/auth/components/base-login-decryption-options.component.ts index 75f6a81b89a..79202054c54 100644 --- a/libs/angular/src/auth/components/base-login-decryption-options.component.ts +++ b/libs/angular/src/auth/components/base-login-decryption-options.component.ts @@ -14,6 +14,10 @@ import { throwError, } from "rxjs"; +import { + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -30,7 +34,6 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { ValidationService } from "@bitwarden/common/platform/abstractions/validation.service"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; enum State { NewUser, @@ -88,6 +91,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { protected validationService: ValidationService, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected platformUtilsService: PlatformUtilsService, + protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected passwordResetEnrollmentService: PasswordResetEnrollmentServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, ) {} @@ -101,14 +105,15 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { await this.setRememberDeviceDefaultValue(); try { - const accountDecryptionOptions: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); + const userDecryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); // see sso-login.strategy - to determine if a user is new or not it just checks if there is a key on the token response.. // can we check if they have a user key or master key in crypto service? Would that be sufficient? if ( - !accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval && - !accountDecryptionOptions?.hasMasterPassword + !userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval && + !userDecryptionOptions?.hasMasterPassword ) { // We are dealing with a new account if: // - User does not have admin approval (i.e. has not enrolled into admin reset) @@ -118,7 +123,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.loadNewUserData(); } else { - this.loadUntrustedDeviceData(accountDecryptionOptions); + this.loadUntrustedDeviceData(userDecryptionOptions); } // Note: this is probably not a comprehensive write up of all scenarios: @@ -195,7 +200,7 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { this.loading = false; } - loadUntrustedDeviceData(accountDecryptionOptions: AccountDecryptionOptions) { + loadUntrustedDeviceData(userDecryptionOptions: UserDecryptionOptions) { this.loading = true; const email$ = from(this.stateService.getEmail()).pipe( @@ -215,13 +220,12 @@ export class BaseLoginDecryptionOptionsComponent implements OnInit, OnDestroy { ) .subscribe((email) => { const showApproveFromOtherDeviceBtn = - accountDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false; + userDecryptionOptions?.trustedDeviceOption?.hasLoginApprovingDevice || false; const showReqAdminApprovalBtn = - !!accountDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false; + !!userDecryptionOptions?.trustedDeviceOption?.hasAdminApproval || false; - const showApproveWithMasterPasswordBtn = - accountDecryptionOptions?.hasMasterPassword || false; + const showApproveWithMasterPasswordBtn = userDecryptionOptions?.hasMasterPassword || false; const userEmail = email; diff --git a/libs/angular/src/auth/components/set-password.component.ts b/libs/angular/src/auth/components/set-password.component.ts index 573c8ad3dd3..a7442f711b8 100644 --- a/libs/angular/src/auth/components/set-password.component.ts +++ b/libs/angular/src/auth/components/set-password.component.ts @@ -1,8 +1,9 @@ import { Directive } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { of } from "rxjs"; +import { firstValueFrom, of } from "rxjs"; import { filter, first, switchMap, tap } from "rxjs/operators"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/organization/organization-api.service.abstraction"; import { OrganizationUserService } from "@bitwarden/common/admin-console/abstractions/organization-user/organization-user.service"; @@ -26,7 +27,6 @@ import { DEFAULT_KDF_CONFIG, } from "@bitwarden/common/platform/enums"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; @@ -64,6 +64,7 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { stateService: StateService, private organizationApiService: OrganizationApiServiceAbstraction, private organizationUserService: OrganizationUserService, + private userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private ssoLoginService: SsoLoginServiceAbstraction, dialogService: DialogService, ) { @@ -228,11 +229,11 @@ export class SetPasswordComponent extends BaseChangePasswordComponent { await this.stateService.setForceSetPasswordReason(ForceSetPasswordReason.None); // User now has a password so update account decryption options in state - const acctDecryptionOpts: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); - - acctDecryptionOpts.hasMasterPassword = true; - await this.stateService.setAccountDecryptionOptions(acctDecryptionOpts); + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + userDecryptionOpts.hasMasterPassword = true; + await this.userDecryptionOptionsService.setUserDecryptionOptions(userDecryptionOpts); await this.stateService.setKdfType(this.kdf); await this.stateService.setKdfConfig(this.kdfConfig); diff --git a/libs/angular/src/auth/components/sso.component.spec.ts b/libs/angular/src/auth/components/sso.component.spec.ts index 9c31b6681a5..82650cb7f18 100644 --- a/libs/angular/src/auth/components/sso.component.spec.ts +++ b/libs/angular/src/auth/components/sso.component.spec.ts @@ -2,16 +2,20 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { MockProxy, mock } from "jest-mock-extended"; -import { Observable, of } from "rxjs"; +import { BehaviorSubject, Observable, of } from "rxjs"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, + LoginStrategyServiceAbstraction, + FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption, + FakeUserDecryptionOptions as UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; -import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { EnvironmentService } from "@bitwarden/common/platform/abstractions/environment.service"; @@ -19,7 +23,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { SsoComponent } from "./sso.component"; @@ -62,6 +65,7 @@ describe("SsoComponent", () => { let mockEnvironmentService: MockProxy; let mockPasswordGenerationService: MockProxy; let mockLogService: MockProxy; + let mockUserDecryptionOptionsService: MockProxy; let mockConfigService: MockProxy; // Mock authService.logIn params @@ -77,17 +81,19 @@ describe("SsoComponent", () => { let mockOnSuccessfulLoginForceResetNavigate: jest.Mock; let mockOnSuccessfulLoginTdeNavigate: jest.Mock; - let mockAcctDecryptionOpts: { - noMasterPassword: AccountDecryptionOptions; - withMasterPassword: AccountDecryptionOptions; - withMasterPasswordAndTrustedDevice: AccountDecryptionOptions; - withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; - withMasterPasswordAndKeyConnector: AccountDecryptionOptions; - noMasterPasswordWithTrustedDevice: AccountDecryptionOptions; - noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; - noMasterPasswordWithKeyConnector: AccountDecryptionOptions; + let mockUserDecryptionOpts: { + noMasterPassword: UserDecryptionOptions; + withMasterPassword: UserDecryptionOptions; + withMasterPasswordAndTrustedDevice: UserDecryptionOptions; + withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions; + withMasterPasswordAndKeyConnector: UserDecryptionOptions; + noMasterPasswordWithTrustedDevice: UserDecryptionOptions; + noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions; + noMasterPasswordWithKeyConnector: UserDecryptionOptions; }; + let selectedUserDecryptionOptions: BehaviorSubject; + beforeEach(() => { // Mock Services mockLoginStrategyService = mock(); @@ -109,6 +115,7 @@ describe("SsoComponent", () => { mockEnvironmentService = mock(); mockPasswordGenerationService = mock(); mockLogService = mock(); + mockUserDecryptionOptionsService = mock(); mockConfigService = mock(); // Mock loginStrategyService.logIn params @@ -124,49 +131,52 @@ describe("SsoComponent", () => { mockOnSuccessfulLoginForceResetNavigate = jest.fn(); mockOnSuccessfulLoginTdeNavigate = jest.fn(); - mockAcctDecryptionOpts = { - noMasterPassword: new AccountDecryptionOptions({ + mockUserDecryptionOpts = { + noMasterPassword: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: undefined, keyConnectorOption: undefined, }), - withMasterPassword: new AccountDecryptionOptions({ + withMasterPassword: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: undefined, keyConnectorOption: undefined, }), - withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({ + withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), keyConnectorOption: undefined, }), - withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ + withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), keyConnectorOption: undefined, }), - withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({ + withMasterPasswordAndKeyConnector: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: undefined, keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), }), - noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({ + noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), keyConnectorOption: undefined, }), - noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ + noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), keyConnectorOption: undefined, }), - noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({ + noMasterPasswordWithKeyConnector: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: undefined, keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), }), }; + selectedUserDecryptionOptions = new BehaviorSubject(null); + mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; + TestBed.configureTestingModule({ declarations: [TestSsoComponent], providers: [ @@ -183,6 +193,10 @@ describe("SsoComponent", () => { { provide: EnvironmentService, useValue: mockEnvironmentService }, { provide: PasswordGenerationServiceAbstraction, useValue: mockPasswordGenerationService }, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: mockUserDecryptionOptionsService, + }, { provide: LogService, useValue: mockLogService }, { provide: ConfigServiceAbstraction, useValue: mockConfigService }, ], @@ -230,9 +244,7 @@ describe("SsoComponent", () => { authResult.twoFactorProviders = new Map([[TwoFactorProviderType.Authenticator, {}]]); // use standard user with MP because this test is not concerned with password reset. - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); mockLoginStrategyService.logIn.mockResolvedValue(authResult); }); @@ -341,8 +353,8 @@ describe("SsoComponent", () => { describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { let authResult; beforeEach(() => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, ); authResult = new AuthResult(); @@ -377,8 +389,8 @@ describe("SsoComponent", () => { const reasonString = ForceSetPasswordReason[forceResetPasswordReason]; let authResult; beforeEach(() => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice, ); authResult = new AuthResult(); @@ -394,8 +406,8 @@ describe("SsoComponent", () => { describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => { let authResult; beforeEach(() => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice, ); authResult = new AuthResult(); @@ -440,9 +452,7 @@ describe("SsoComponent", () => { describe("Given user needs to set a master password", () => { beforeEach(() => { // Only need to test the case where the user has no master password to test the primary change mp flow here - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword); }); testChangePasswordOnSuccessfulLogin(); @@ -450,9 +460,7 @@ describe("SsoComponent", () => { }); it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPasswordWithKeyConnector); await _component.logIn(code, codeVerifier, orgIdFromState); expect(mockLoginStrategyService.logIn).toHaveBeenCalledTimes(1); @@ -475,9 +483,7 @@ describe("SsoComponent", () => { beforeEach(() => { // use standard user with MP because this test is not concerned with password reset. - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); const authResult = new AuthResult(); authResult.forcePasswordReset = forceResetPasswordReason; @@ -494,9 +500,7 @@ describe("SsoComponent", () => { const authResult = new AuthResult(); authResult.twoFactorProviders = null; // use standard user with MP because this test is not concerned with password reset. - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); authResult.forcePasswordReset = ForceSetPasswordReason.None; mockLoginStrategyService.logIn.mockResolvedValue(authResult); }); diff --git a/libs/angular/src/auth/components/sso.component.ts b/libs/angular/src/auth/components/sso.component.ts index a5a08f9aefa..f6cd334cf69 100644 --- a/libs/angular/src/auth/components/sso.component.ts +++ b/libs/angular/src/auth/components/sso.component.ts @@ -1,13 +1,19 @@ import { Directive } from "@angular/core"; import { ActivatedRoute, NavigationExtras, Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { first } from "rxjs/operators"; -import { LoginStrategyServiceAbstraction, SsoLoginCredentials } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + SsoLoginCredentials, + TrustedDeviceUserDecryptionOption, + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { SsoPreValidateResponse } from "@bitwarden/common/auth/models/response/sso-pre-validate.response"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -17,7 +23,6 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; @Directive() @@ -59,6 +64,7 @@ export class SsoComponent { protected environmentService: EnvironmentService, protected passwordGenerationService: PasswordGenerationServiceAbstraction, protected logService: LogService, + protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected configService: ConfigServiceAbstraction, ) {} @@ -194,9 +200,6 @@ export class SsoComponent { this.formPromise = this.loginStrategyService.logIn(credentials); const authResult = await this.formPromise; - const acctDecryptionOpts: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); - if (authResult.requiresTwoFactor) { return await this.handleTwoFactorRequired(orgSsoIdentifier); } @@ -217,15 +220,20 @@ export class SsoComponent { return await this.handleForcePasswordReset(orgSsoIdentifier); } + // must come after 2fa check since user decryption options aren't available if 2fa is required + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); + const tdeEnabled = await this.isTrustedDeviceEncEnabled( - acctDecryptionOpts.trustedDeviceOption, + userDecryptionOpts.trustedDeviceOption, ); if (tdeEnabled) { return await this.handleTrustedDeviceEncryptionEnabled( authResult, orgSsoIdentifier, - acctDecryptionOpts, + userDecryptionOpts, ); } @@ -233,8 +241,8 @@ export class SsoComponent { // have one and they aren't using key connector. // Note: TDE & Key connector are mutually exclusive org config options. const requireSetPassword = - !acctDecryptionOpts.hasMasterPassword && - acctDecryptionOpts.keyConnectorOption === undefined; + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.keyConnectorOption === undefined; if (requireSetPassword || authResult.resetMasterPassword) { // Change implies going no password -> password in this case @@ -270,12 +278,12 @@ export class SsoComponent { private async handleTrustedDeviceEncryptionEnabled( authResult: AuthResult, orgIdentifier: string, - acctDecryptionOpts: AccountDecryptionOptions, + userDecryptionOpts: UserDecryptionOptions, ): Promise { // If user doesn't have a MP, but has reset password permission, they must set a MP if ( - !acctDecryptionOpts.hasMasterPassword && - acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission ) { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate in this scenario as we are in a pre-decryption state, and diff --git a/libs/angular/src/auth/components/two-factor.component.spec.ts b/libs/angular/src/auth/components/two-factor.component.spec.ts index bf9489ba77f..c27ba7082f0 100644 --- a/libs/angular/src/auth/components/two-factor.component.spec.ts +++ b/libs/angular/src/auth/components/two-factor.component.spec.ts @@ -1,19 +1,24 @@ import { Component } from "@angular/core"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, Router, convertToParamMap } from "@angular/router"; -import { MockProxy, mock } from "jest-mock-extended"; +import { ActivatedRoute, convertToParamMap, Router } from "@angular/router"; +import { mock, MockProxy } from "jest-mock-extended"; +import { BehaviorSubject } from "rxjs"; // eslint-disable-next-line no-restricted-imports import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + FakeKeyConnectorUserDecryptionOption as KeyConnectorUserDecryptionOption, + LoginStrategyServiceAbstraction, + FakeTrustedDeviceUserDecryptionOption as TrustedDeviceUserDecryptionOption, + FakeUserDecryptionOptions as UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; import { TwoFactorService } from "@bitwarden/common/auth/abstractions/two-factor.service"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { KeyConnectorUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; -import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { AppIdService } from "@bitwarden/common/platform/abstractions/app-id.service"; import { ConfigServiceAbstraction } from "@bitwarden/common/platform/abstractions/config/config.service.abstraction"; @@ -22,7 +27,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { TwoFactorComponent } from "./two-factor.component"; @@ -56,20 +60,23 @@ describe("TwoFactorComponent", () => { let mockTwoFactorService: MockProxy; let mockAppIdService: MockProxy; let mockLoginService: MockProxy; + let mockUserDecryptionOptionsService: MockProxy; let mockSsoLoginService: MockProxy; let mockConfigService: MockProxy; - let mockAcctDecryptionOpts: { - noMasterPassword: AccountDecryptionOptions; - withMasterPassword: AccountDecryptionOptions; - withMasterPasswordAndTrustedDevice: AccountDecryptionOptions; - withMasterPasswordAndTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; - withMasterPasswordAndKeyConnector: AccountDecryptionOptions; - noMasterPasswordWithTrustedDevice: AccountDecryptionOptions; - noMasterPasswordWithTrustedDeviceWithManageResetPassword: AccountDecryptionOptions; - noMasterPasswordWithKeyConnector: AccountDecryptionOptions; + let mockUserDecryptionOpts: { + noMasterPassword: UserDecryptionOptions; + withMasterPassword: UserDecryptionOptions; + withMasterPasswordAndTrustedDevice: UserDecryptionOptions; + withMasterPasswordAndTrustedDeviceWithManageResetPassword: UserDecryptionOptions; + withMasterPasswordAndKeyConnector: UserDecryptionOptions; + noMasterPasswordWithTrustedDevice: UserDecryptionOptions; + noMasterPasswordWithTrustedDeviceWithManageResetPassword: UserDecryptionOptions; + noMasterPasswordWithKeyConnector: UserDecryptionOptions; }; + let selectedUserDecryptionOptions: BehaviorSubject; + beforeEach(() => { mockLoginStrategyService = mock(); mockRouter = mock(); @@ -83,52 +90,56 @@ describe("TwoFactorComponent", () => { mockTwoFactorService = mock(); mockAppIdService = mock(); mockLoginService = mock(); + mockUserDecryptionOptionsService = mock(); mockSsoLoginService = mock(); mockConfigService = mock(); - mockAcctDecryptionOpts = { - noMasterPassword: new AccountDecryptionOptions({ + mockUserDecryptionOpts = { + noMasterPassword: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: undefined, keyConnectorOption: undefined, }), - withMasterPassword: new AccountDecryptionOptions({ + withMasterPassword: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: undefined, keyConnectorOption: undefined, }), - withMasterPasswordAndTrustedDevice: new AccountDecryptionOptions({ + withMasterPasswordAndTrustedDevice: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), keyConnectorOption: undefined, }), - withMasterPasswordAndTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ + withMasterPasswordAndTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), keyConnectorOption: undefined, }), - withMasterPasswordAndKeyConnector: new AccountDecryptionOptions({ + withMasterPasswordAndKeyConnector: new UserDecryptionOptions({ hasMasterPassword: true, trustedDeviceOption: undefined, keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), }), - noMasterPasswordWithTrustedDevice: new AccountDecryptionOptions({ + noMasterPasswordWithTrustedDevice: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, false), keyConnectorOption: undefined, }), - noMasterPasswordWithTrustedDeviceWithManageResetPassword: new AccountDecryptionOptions({ + noMasterPasswordWithTrustedDeviceWithManageResetPassword: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: new TrustedDeviceUserDecryptionOption(true, false, true), keyConnectorOption: undefined, }), - noMasterPasswordWithKeyConnector: new AccountDecryptionOptions({ + noMasterPasswordWithKeyConnector: new UserDecryptionOptions({ hasMasterPassword: false, trustedDeviceOption: undefined, keyConnectorOption: new KeyConnectorUserDecryptionOption("http://example.com"), }), }; + selectedUserDecryptionOptions = new BehaviorSubject(null); + mockUserDecryptionOptionsService.userDecryptionOptions$ = selectedUserDecryptionOptions; + TestBed.configureTestingModule({ declarations: [TestTwoFactorComponent], providers: [ @@ -153,6 +164,10 @@ describe("TwoFactorComponent", () => { { provide: TwoFactorService, useValue: mockTwoFactorService }, { provide: AppIdService, useValue: mockAppIdService }, { provide: LoginService, useValue: mockLoginService }, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: mockUserDecryptionOptionsService, + }, { provide: SsoLoginServiceAbstraction, useValue: mockSsoLoginService }, { provide: ConfigServiceAbstraction, useValue: mockConfigService }, ], @@ -213,9 +228,7 @@ describe("TwoFactorComponent", () => { component.remember = remember; component.captchaToken = captchaToken; - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); }); it("calls authService.logInTwoFactor with correct parameters when form is submitted", async () => { @@ -289,17 +302,15 @@ describe("TwoFactorComponent", () => { describe("Given user needs to set a master password", () => { beforeEach(() => { // Only need to test the case where the user has no master password to test the primary change mp flow here - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.noMasterPassword); }); testChangePasswordOnSuccessfulLogin(); }); it("does not navigate to the change password route when the user has key connector even if user has no master password", async () => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPasswordWithKeyConnector, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithKeyConnector, ); await component.doSubmit(); @@ -321,9 +332,7 @@ describe("TwoFactorComponent", () => { beforeEach(() => { // use standard user with MP because this test is not concerned with password reset. - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPassword, - ); + selectedUserDecryptionOptions.next(mockUserDecryptionOpts.withMasterPassword); const authResult = new AuthResult(); authResult.forcePasswordReset = forceResetPasswordReason; @@ -385,8 +394,8 @@ describe("TwoFactorComponent", () => { describe("Given Trusted Device Encryption is enabled and user needs to set a master password", () => { beforeEach(() => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.noMasterPasswordWithTrustedDeviceWithManageResetPassword, ); const authResult = new AuthResult(); @@ -420,8 +429,8 @@ describe("TwoFactorComponent", () => { beforeEach(() => { // use standard user with MP because this test is not concerned with password reset. - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice, ); const authResult = new AuthResult(); @@ -436,8 +445,8 @@ describe("TwoFactorComponent", () => { describe("Given Trusted Device Encryption is enabled, user doesn't need to set a MP, and forcePasswordReset is not required", () => { let authResult; beforeEach(() => { - mockStateService.getAccountDecryptionOptions.mockResolvedValue( - mockAcctDecryptionOpts.withMasterPasswordAndTrustedDevice, + selectedUserDecryptionOptions.next( + mockUserDecryptionOpts.withMasterPasswordAndTrustedDevice, ); authResult = new AuthResult(); diff --git a/libs/angular/src/auth/components/two-factor.component.ts b/libs/angular/src/auth/components/two-factor.component.ts index c697d9ceb0a..fdeeb238e92 100644 --- a/libs/angular/src/auth/components/two-factor.component.ts +++ b/libs/angular/src/auth/components/two-factor.component.ts @@ -6,7 +6,12 @@ import { first } from "rxjs/operators"; // eslint-disable-next-line no-restricted-imports import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { LoginStrategyServiceAbstraction } from "@bitwarden/auth/common"; +import { + LoginStrategyServiceAbstraction, + TrustedDeviceUserDecryptionOption, + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { LoginService } from "@bitwarden/common/auth/abstractions/login.service"; import { SsoLoginServiceAbstraction } from "@bitwarden/common/auth/abstractions/sso-login.service.abstraction"; @@ -15,7 +20,6 @@ import { AuthenticationType } from "@bitwarden/common/auth/enums/authentication- import { TwoFactorProviderType } from "@bitwarden/common/auth/enums/two-factor-provider-type"; import { AuthResult } from "@bitwarden/common/auth/models/domain/auth-result"; import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; -import { TrustedDeviceUserDecryptionOption } from "@bitwarden/common/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; import { TokenTwoFactorRequest } from "@bitwarden/common/auth/models/request/identity-token/token-two-factor.request"; import { TwoFactorEmailRequest } from "@bitwarden/common/auth/models/request/two-factor-email.request"; import { TwoFactorProviders } from "@bitwarden/common/auth/services/two-factor.service"; @@ -27,7 +31,6 @@ import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.servic import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AccountDecryptionOptions } from "@bitwarden/common/platform/models/domain/account"; import { CaptchaProtectedComponent } from "./captcha-protected.component"; @@ -86,6 +89,7 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI protected twoFactorService: TwoFactorService, protected appIdService: AppIdService, protected loginService: LoginService, + protected userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, protected ssoLoginService: SsoLoginServiceAbstraction, protected configService: ConfigServiceAbstraction, ) { @@ -290,22 +294,23 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI return await this.handleForcePasswordReset(this.orgIdentifier); } - const acctDecryptionOpts: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); + const userDecryptionOpts = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); - const tdeEnabled = await this.isTrustedDeviceEncEnabled(acctDecryptionOpts.trustedDeviceOption); + const tdeEnabled = await this.isTrustedDeviceEncEnabled(userDecryptionOpts.trustedDeviceOption); if (tdeEnabled) { return await this.handleTrustedDeviceEncryptionEnabled( authResult, this.orgIdentifier, - acctDecryptionOpts, + userDecryptionOpts, ); } // User must set password if they don't have one and they aren't using either TDE or key connector. const requireSetPassword = - !acctDecryptionOpts.hasMasterPassword && acctDecryptionOpts.keyConnectorOption === undefined; + !userDecryptionOpts.hasMasterPassword && userDecryptionOpts.keyConnectorOption === undefined; if (requireSetPassword || authResult.resetMasterPassword) { // Change implies going no password -> password in this case @@ -326,12 +331,12 @@ export class TwoFactorComponent extends CaptchaProtectedComponent implements OnI private async handleTrustedDeviceEncryptionEnabled( authResult: AuthResult, orgIdentifier: string, - acctDecryptionOpts: AccountDecryptionOptions, + userDecryptionOpts: UserDecryptionOptions, ): Promise { // If user doesn't have a MP, but has reset password permission, they must set a MP if ( - !acctDecryptionOpts.hasMasterPassword && - acctDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission + !userDecryptionOpts.hasMasterPassword && + userDecryptionOpts.trustedDeviceOption.hasManageResetPasswordPermission ) { // Set flag so that auth guard can redirect to set password screen after decryption (trusted or untrusted device) // Note: we cannot directly navigate to the set password screen in this scenario as we are in a pre-decryption state, and diff --git a/libs/angular/src/auth/guards/lock.guard.ts b/libs/angular/src/auth/guards/lock.guard.ts index f77a6c71e15..6f71d77a63d 100644 --- a/libs/angular/src/auth/guards/lock.guard.ts +++ b/libs/angular/src/auth/guards/lock.guard.ts @@ -53,7 +53,7 @@ export function lockGuard(): CanActivateFn { // User is authN and in locked state. - const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); + const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); // Create special exception which allows users to go from the login-initiated page to the lock page for the approve w/ MP flow // The MP check is necessary to prevent direct manual navigation from other locked state pages for users who don't have a MP diff --git a/libs/angular/src/auth/guards/redirect.guard.ts b/libs/angular/src/auth/guards/redirect.guard.ts index 504fcb3f36f..ca9152186d0 100644 --- a/libs/angular/src/auth/guards/redirect.guard.ts +++ b/libs/angular/src/auth/guards/redirect.guard.ts @@ -46,7 +46,7 @@ export function redirectGuard(overrides: Partial = {}): CanActiv // If locked, TDE is enabled, and the user hasn't decrypted yet, then redirect to the // login decryption options component. - const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); + const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus === AuthenticationStatus.Locked && tdeEnabled && !everHadUserKey) { return router.createUrlTree([routes.notDecrypted], { queryParams: route.queryParams }); diff --git a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts index b2b33fcd77f..146c6e19a21 100644 --- a/libs/angular/src/auth/guards/tde-decryption-required.guard.ts +++ b/libs/angular/src/auth/guards/tde-decryption-required.guard.ts @@ -26,7 +26,7 @@ export function tdeDecryptionRequiredGuard(): CanActivateFn { const router = inject(Router); const authStatus = await authService.getAuthStatus(); - const tdeEnabled = await deviceTrustCryptoService.supportsDeviceTrust(); + const tdeEnabled = await firstValueFrom(deviceTrustCryptoService.supportsDeviceTrust$); const everHadUserKey = await firstValueFrom(cryptoService.everHadUserKey$); if (authStatus !== AuthenticationStatus.Locked || !tdeEnabled || everHadUserKey) { return router.createUrlTree(["/"]); diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 498c9171b3e..18d5e1136ab 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -8,6 +8,9 @@ import { PinCryptoService, LoginStrategyServiceAbstraction, LoginStrategyService, + InternalUserDecryptionOptionsServiceAbstraction, + UserDecryptionOptionsService, + UserDecryptionOptionsServiceAbstraction, } from "@bitwarden/auth/common"; import { ApiService as ApiServiceAbstraction } from "@bitwarden/common/abstractions/api.service"; import { AuditService as AuditServiceAbstraction } from "@bitwarden/common/abstractions/audit.service"; @@ -243,8 +246,8 @@ import { safeProvider, SafeProvider } from "../platform/utils/safe-provider"; import { LOCALES_DIRECTORY, LOCKED_CALLBACK, - LOG_MAC_FAILURES, LOGOUT_CALLBACK, + LOG_MAC_FAILURES, MEMORY_STORAGE, OBSERVABLE_DISK_STORAGE, OBSERVABLE_MEMORY_STORAGE, @@ -369,6 +372,7 @@ const typesafeProviders: Array = [ PolicyServiceAbstraction, DeviceTrustCryptoServiceAbstraction, AuthRequestServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, GlobalStateProvider, BillingAccountProfileStateService, ], @@ -477,6 +481,15 @@ const typesafeProviders: Array = [ useClass: EnvironmentService, deps: [StateProvider, AccountServiceAbstraction], }), + safeProvider({ + provide: InternalUserDecryptionOptionsServiceAbstraction, + useClass: UserDecryptionOptionsService, + deps: [StateProvider], + }), + safeProvider({ + provide: UserDecryptionOptionsServiceAbstraction, + useExisting: InternalUserDecryptionOptionsServiceAbstraction, + }), safeProvider({ provide: TotpServiceAbstraction, useClass: TotpService, @@ -577,6 +590,7 @@ const typesafeProviders: Array = [ FolderApiServiceAbstraction, InternalOrganizationServiceAbstraction, SendApiServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, AvatarServiceAbstraction, LOGOUT_CALLBACK, BillingAccountProfileStateService, @@ -587,6 +601,7 @@ const typesafeProviders: Array = [ provide: VaultTimeoutSettingsServiceAbstraction, useClass: VaultTimeoutSettingsService, deps: [ + UserDecryptionOptionsServiceAbstraction, CryptoServiceAbstraction, TokenServiceAbstraction, PolicyServiceAbstraction, @@ -765,6 +780,7 @@ const typesafeProviders: Array = [ CryptoServiceAbstraction, I18nServiceAbstraction, UserVerificationApiServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, PinCryptoServiceAbstraction, LogService, VaultTimeoutSettingsServiceAbstraction, @@ -902,6 +918,7 @@ const typesafeProviders: Array = [ DevicesApiServiceAbstraction, I18nServiceAbstraction, PlatformUtilsServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, ], }), safeProvider({ diff --git a/libs/auth/src/common/abstractions/index.ts b/libs/auth/src/common/abstractions/index.ts index c4b5bed1284..1feee6695a9 100644 --- a/libs/auth/src/common/abstractions/index.ts +++ b/libs/auth/src/common/abstractions/index.ts @@ -1,3 +1,4 @@ export * from "./pin-crypto.service.abstraction"; export * from "./login-strategy.service"; +export * from "./user-decryption-options.service.abstraction"; export * from "./auth-request.service.abstraction"; diff --git a/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts new file mode 100644 index 00000000000..e46fb09cff6 --- /dev/null +++ b/libs/auth/src/common/abstractions/user-decryption-options.service.abstraction.ts @@ -0,0 +1,34 @@ +import { Observable } from "rxjs"; + +import { UserDecryptionOptions } from "../models"; + +export abstract class UserDecryptionOptionsServiceAbstraction { + /** + * Returns what decryption options are available for the current user. + * @remark This is sent from the server on authentication. + */ + abstract userDecryptionOptions$: Observable; + /** + * Uses user decryption options to determine if current user has a master password. + * @remark This is sent from the server, and does not indicate if the master password + * was used to login and/or if a master key is saved locally. + */ + abstract hasMasterPassword$: Observable; + + /** + * Returns the user decryption options for the given user id. + * @param userId The user id to check. + */ + abstract userDecryptionOptionsById$(userId: string): Observable; +} + +export abstract class InternalUserDecryptionOptionsServiceAbstraction extends UserDecryptionOptionsServiceAbstraction { + /** + * Sets the current decryption options for the user, contains the current configuration + * of the users account related to how they can decrypt their vault. + * @remark Intended to be used when user decryption options are received from server, does + * not update the server. Consider syncing instead of updating locally. + * @param userDecryptionOptions Current user decryption options received from server. + */ + abstract setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise; +} diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts index 18ac9f0bf78..53722cd259c 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.spec.ts @@ -17,6 +17,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { @@ -37,6 +38,7 @@ describe("AuthRequestLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptions: MockProxy; let deviceTrustCryptoService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -65,6 +67,7 @@ describe("AuthRequestLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptions = mock(); deviceTrustCryptoService = mock(); billingAccountProfileStateService = mock(); @@ -83,6 +86,7 @@ describe("AuthRequestLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptions, deviceTrustCryptoService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts index 09312226d8b..c42f43e7643 100644 --- a/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/auth-request-login.strategy.ts @@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -54,6 +55,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { @@ -67,6 +69,7 @@ export class AuthRequestLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/login.strategy.spec.ts b/libs/auth/src/common/login-strategies/login.strategy.spec.ts index 6f3d480f201..ed40797df51 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.spec.ts @@ -28,7 +28,6 @@ import { AccountProfile, AccountTokens, AccountKeys, - AccountDecryptionOptions, } from "@bitwarden/common/platform/models/domain/account"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; @@ -39,8 +38,10 @@ import { import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserKey, MasterKey, DeviceKey } from "@bitwarden/common/types/key"; -import { LoginStrategyServiceAbstraction } from "../abstractions/login-strategy.service"; -import { PasswordLoginCredentials } from "../models/domain/login-credentials"; +import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; +import { PasswordLoginCredentials } from "../models"; +import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { PasswordLoginStrategy, PasswordLoginStrategyData } from "./password-login.strategy"; @@ -108,6 +109,7 @@ describe("LoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -126,7 +128,7 @@ describe("LoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); - + userDecryptionOptionsService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -146,6 +148,7 @@ describe("LoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, @@ -204,9 +207,11 @@ describe("LoginStrategy", () => { ...new AccountTokens(), }, keys: new AccountKeys(), - decryptionOptions: AccountDecryptionOptions.fromResponse(idTokenResponse), }), ); + expect(userDecryptionOptionsService.setUserDecryptionOptions).toHaveBeenCalledWith( + UserDecryptionOptions.fromResponse(idTokenResponse), + ); expect(messagingService.send).toHaveBeenCalledWith("loggedIn"); }); @@ -409,6 +414,7 @@ describe("LoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, diff --git a/libs/auth/src/common/login-strategies/login.strategy.ts b/libs/auth/src/common/login-strategies/login.strategy.ts index f5f28dd0440..eef5626493b 100644 --- a/libs/auth/src/common/login-strategies/login.strategy.ts +++ b/libs/auth/src/common/login-strategies/login.strategy.ts @@ -30,9 +30,9 @@ import { Account, AccountProfile, AccountTokens, - AccountDecryptionOptions, } from "@bitwarden/common/platform/models/domain/account"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials, PasswordLoginCredentials, @@ -40,6 +40,7 @@ import { AuthRequestLoginCredentials, WebAuthnLoginCredentials, } from "../models/domain/login-credentials"; +import { UserDecryptionOptions } from "../models/domain/user-decryption-options"; import { CacheData } from "../services/login-strategies/login-strategy.state"; type IdentityResponse = IdentityTokenResponse | IdentityTwoFactorResponse | IdentityCaptchaResponse; @@ -69,6 +70,7 @@ export abstract class LoginStrategy { protected logService: LogService, protected stateService: StateService, protected twoFactorService: TwoFactorService, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected billingAccountProfileStateService: BillingAccountProfileStateService, ) {} @@ -203,11 +205,14 @@ export abstract class LoginStrategy { ...new AccountTokens(), }, keys: accountKeys, - decryptionOptions: AccountDecryptionOptions.fromResponse(tokenResponse), adminAuthRequest: adminAuthRequest?.toJSON(), }), ); + await this.userDecryptionOptionsService.setUserDecryptionOptions( + UserDecryptionOptions.fromResponse(tokenResponse), + ); + await this.billingAccountProfileStateService.setHasPremium(accountInformation.premium, false); } diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts index 007c33afc6b..470a4ac713c 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.spec.ts @@ -27,6 +27,7 @@ import { CsprngArray } from "@bitwarden/common/types/csprng"; import { MasterKey, UserKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -60,6 +61,7 @@ describe("PasswordLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let policyService: MockProxy; let passwordStrengthService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -79,6 +81,7 @@ describe("PasswordLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); policyService = mock(); passwordStrengthService = mock(); billingAccountProfileStateService = mock(); @@ -108,6 +111,7 @@ describe("PasswordLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, passwordStrengthService, policyService, loginStrategyService, diff --git a/libs/auth/src/common/login-strategies/password-login.strategy.ts b/libs/auth/src/common/login-strategies/password-login.strategy.ts index 2104595b450..be93d39ebc4 100644 --- a/libs/auth/src/common/login-strategies/password-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/password-login.strategy.ts @@ -26,6 +26,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { MasterKey } from "@bitwarden/common/types/key"; import { LoginStrategyServiceAbstraction } from "../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { PasswordLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -84,6 +85,7 @@ export class PasswordLoginStrategy extends LoginStrategy { logService: LogService, protected stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private passwordStrengthService: PasswordStrengthServiceAbstraction, private policyService: PolicyService, private loginStrategyService: LoginStrategyServiceAbstraction, @@ -99,6 +101,7 @@ export class PasswordLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts index c987bcc95a6..d4b0b13eafe 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.spec.ts @@ -23,7 +23,10 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { DeviceKey, UserKey, MasterKey } from "@bitwarden/common/types/key"; -import { AuthRequestServiceAbstraction } from "../abstractions"; +import { + AuthRequestServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, +} from "../abstractions"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -39,6 +42,7 @@ describe("SsoLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; let deviceTrustCryptoService: MockProxy; let authRequestService: MockProxy; @@ -66,6 +70,7 @@ describe("SsoLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); keyConnectorService = mock(); deviceTrustCryptoService = mock(); authRequestService = mock(); @@ -87,6 +92,7 @@ describe("SsoLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, keyConnectorService, deviceTrustCryptoService, authRequestService, diff --git a/libs/auth/src/common/login-strategies/sso-login.strategy.ts b/libs/auth/src/common/login-strategies/sso-login.strategy.ts index b8d1df6f577..04f158d30a9 100644 --- a/libs/auth/src/common/login-strategies/sso-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/sso-login.strategy.ts @@ -21,7 +21,10 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; -import { AuthRequestServiceAbstraction } from "../abstractions"; +import { + InternalUserDecryptionOptionsServiceAbstraction, + AuthRequestServiceAbstraction, +} from "../abstractions"; import { SsoLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -84,6 +87,7 @@ export class SsoLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private keyConnectorService: KeyConnectorService, private deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, private authRequestService: AuthRequestServiceAbstraction, @@ -100,6 +104,7 @@ export class SsoLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts index 48f6fd32aba..432eeb9aab9 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.spec.ts @@ -18,6 +18,7 @@ import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/sym import { CsprngArray } from "@bitwarden/common/types/csprng"; import { UserKey, MasterKey } from "@bitwarden/common/types/key"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -35,6 +36,7 @@ describe("UserApiLoginStrategy", () => { let logService: MockProxy; let stateService: MockProxy; let twoFactorService: MockProxy; + let userDecryptionOptionsService: MockProxy; let keyConnectorService: MockProxy; let environmentService: MockProxy; let billingAccountProfileStateService: MockProxy; @@ -57,6 +59,7 @@ describe("UserApiLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); keyConnectorService = mock(); environmentService = mock(); billingAccountProfileStateService = mock(); @@ -76,6 +79,7 @@ describe("UserApiLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, environmentService, keyConnectorService, billingAccountProfileStateService, diff --git a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts index 9bb6d8fb125..730243aafc6 100644 --- a/libs/auth/src/common/login-strategies/user-api-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/user-api-login.strategy.ts @@ -17,6 +17,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { UserApiLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -47,6 +48,7 @@ export class UserApiLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, private environmentService: EnvironmentService, private keyConnectorService: KeyConnectorService, billingAccountProfileStateService: BillingAccountProfileStateService, @@ -61,6 +63,7 @@ export class UserApiLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); this.cache = new BehaviorSubject(data); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts index 9ab64170c1d..edc1441361a 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.spec.ts @@ -18,6 +18,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { PrfKey, UserKey } from "@bitwarden/common/types/key"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions/user-decryption-options.service.abstraction"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { identityTokenResponseFactory } from "./login.strategy.spec"; @@ -35,6 +36,7 @@ describe("WebAuthnLoginStrategy", () => { let logService!: MockProxy; let stateService!: MockProxy; let twoFactorService!: MockProxy; + let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; let webAuthnLoginStrategy!: WebAuthnLoginStrategy; @@ -70,6 +72,7 @@ describe("WebAuthnLoginStrategy", () => { logService = mock(); stateService = mock(); twoFactorService = mock(); + userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); tokenService.getTwoFactorToken.mockResolvedValue(null); @@ -87,6 +90,7 @@ describe("WebAuthnLoginStrategy", () => { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts index b60342f0b41..a8e67597b82 100644 --- a/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts +++ b/libs/auth/src/common/login-strategies/webauthn-login.strategy.ts @@ -17,6 +17,7 @@ import { StateService } from "@bitwarden/common/platform/abstractions/state.serv import { SymmetricCryptoKey } from "@bitwarden/common/platform/models/domain/symmetric-crypto-key"; import { UserKey } from "@bitwarden/common/types/key"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../abstractions"; import { WebAuthnLoginCredentials } from "../models/domain/login-credentials"; import { CacheData } from "../services/login-strategies/login-strategy.state"; @@ -49,6 +50,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { logService: LogService, stateService: StateService, twoFactorService: TwoFactorService, + userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, billingAccountProfileStateService: BillingAccountProfileStateService, ) { super( @@ -61,6 +63,7 @@ export class WebAuthnLoginStrategy extends LoginStrategy { logService, stateService, twoFactorService, + userDecryptionOptionsService, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/models/domain/index.ts b/libs/auth/src/common/models/domain/index.ts index c3166f737d6..b8b83711a4a 100644 --- a/libs/auth/src/common/models/domain/index.ts +++ b/libs/auth/src/common/models/domain/index.ts @@ -1,2 +1,3 @@ export * from "./rotateable-key-set"; export * from "./login-credentials"; +export * from "./user-decryption-options"; diff --git a/libs/auth/src/common/models/domain/user-decryption-options.ts b/libs/auth/src/common/models/domain/user-decryption-options.ts new file mode 100644 index 00000000000..c600c8be476 --- /dev/null +++ b/libs/auth/src/common/models/domain/user-decryption-options.ts @@ -0,0 +1,153 @@ +import { Jsonify } from "type-fest"; + +import { KeyConnectorUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/key-connector-user-decryption-option.response"; +import { TrustedDeviceUserDecryptionOptionResponse } from "@bitwarden/common/auth/models/response/user-decryption-options/trusted-device-user-decryption-option.response"; +import { IdentityTokenResponse } from "@bitwarden/common/src/auth/models/response/identity-token.response"; + +/** + * Key Connector decryption options. Intended to be sent to the client for use after authentication. + * @see {@link UserDecryptionOptions} + */ +export class KeyConnectorUserDecryptionOption { + /** The URL of the key connector configured for this user. */ + keyConnectorUrl: string; + + /** + * Initializes a new instance of the KeyConnectorUserDecryptionOption from a response object. + * @param response The key connector user decryption option response object. + * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the response is nullish. + */ + static fromResponse( + response: KeyConnectorUserDecryptionOptionResponse, + ): KeyConnectorUserDecryptionOption { + const options = new KeyConnectorUserDecryptionOption(); + options.keyConnectorUrl = response?.keyConnectorUrl ?? null; + return options; + } + + /** + * Initializes a new instance of a KeyConnectorUserDecryptionOption from a JSON object. + * @param obj JSON object to deserialize. + * @returns A new instance of the KeyConnectorUserDecryptionOption. Will initialize even if the JSON object is nullish. + */ + static fromJSON( + obj: Jsonify, + ): KeyConnectorUserDecryptionOption { + return Object.assign(new KeyConnectorUserDecryptionOption(), obj); + } +} + +/** + * Trusted device decryption options. Intended to be sent to the client for use after authentication. + * @see {@link UserDecryptionOptions} + */ +export class TrustedDeviceUserDecryptionOption { + /** True if an admin has approved an admin auth request previously made from this device. */ + hasAdminApproval: boolean; + /** True if the user has a device capable of approving an auth request. */ + hasLoginApprovingDevice: boolean; + /** True if the user has manage reset password permission, as these users must be forced to have a master password. */ + hasManageResetPasswordPermission: boolean; + + /** + * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a response object. + * @param response The trusted device user decryption option response object. + * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the response is nullish. + */ + static fromResponse( + response: TrustedDeviceUserDecryptionOptionResponse, + ): TrustedDeviceUserDecryptionOption { + const options = new TrustedDeviceUserDecryptionOption(); + options.hasAdminApproval = response?.hasAdminApproval ?? false; + options.hasLoginApprovingDevice = response?.hasLoginApprovingDevice ?? false; + options.hasManageResetPasswordPermission = response?.hasManageResetPasswordPermission ?? false; + return options; + } + + /** + * Initializes a new instance of the TrustedDeviceUserDecryptionOption from a JSON object. + * @param obj JSON object to deserialize. + * @returns A new instance of the TrustedDeviceUserDecryptionOption. Will initialize even if the JSON object is nullish. + */ + static fromJSON( + obj: Jsonify, + ): TrustedDeviceUserDecryptionOption { + return Object.assign(new TrustedDeviceUserDecryptionOption(), obj); + } +} + +/** + * Represents the decryption options the user has configured on the server. This is intended to be sent + * to the client on authentication, and can be used to determine how to decrypt the user's vault. + */ +export class UserDecryptionOptions { + /** True if the user has a master password configured on the server. */ + hasMasterPassword: boolean; + /** {@link TrustedDeviceUserDecryptionOption} */ + trustedDeviceOption?: TrustedDeviceUserDecryptionOption; + /** {@link KeyConnectorUserDecryptionOption} */ + keyConnectorOption?: KeyConnectorUserDecryptionOption; + + /** + * Initializes a new instance of the UserDecryptionOptions from a response object. + * @param response user decryption options response object + * @returns A new instance of the UserDecryptionOptions. + * @throws If the response is nullish, this method will throw an error. User decryption options + * are required for client initialization. + */ + // TODO: Change response type to `UserDecryptionOptionsResponse` after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) + static fromResponse(response: IdentityTokenResponse): UserDecryptionOptions { + if (response == null) { + throw new Error("User Decryption Options are required for client initialization."); + } + + const decryptionOptions = new UserDecryptionOptions(); + + if (response.userDecryptionOptions) { + // If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate + // the new decryption options. + const responseOptions = response.userDecryptionOptions; + decryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword; + + decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromResponse( + responseOptions.trustedDeviceOption, + ); + + decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromResponse( + responseOptions.keyConnectorOption, + ); + } else { + // If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so + // we must base our decryption options on the presence of the keyConnectorUrl. + // Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE + // server versions, a master password short-circuited the addition of the keyConnectorUrl to the response. + // TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) + const usingKeyConnector = response.keyConnectorUrl != null; + decryptionOptions.hasMasterPassword = !usingKeyConnector; + if (usingKeyConnector) { + decryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption(); + decryptionOptions.keyConnectorOption.keyConnectorUrl = response.keyConnectorUrl; + } + } + return decryptionOptions; + } + + /** + * Initializes a new instance of the UserDecryptionOptions from a JSON object. + * @param obj JSON object to deserialize. + * @returns A new instance of the UserDecryptionOptions. Will initialize even if the JSON object is nullish. + */ + static fromJSON(obj: Jsonify): UserDecryptionOptions { + const decryptionOptions = Object.assign(new UserDecryptionOptions(), obj); + + decryptionOptions.trustedDeviceOption = TrustedDeviceUserDecryptionOption.fromJSON( + obj?.trustedDeviceOption, + ); + + decryptionOptions.keyConnectorOption = KeyConnectorUserDecryptionOption.fromJSON( + obj?.keyConnectorOption, + ); + + return decryptionOptions; + } +} diff --git a/libs/auth/src/common/models/index.ts b/libs/auth/src/common/models/index.ts index 7886141dc94..e816ee4b883 100644 --- a/libs/auth/src/common/models/index.ts +++ b/libs/auth/src/common/models/index.ts @@ -1 +1,2 @@ export * from "./domain"; +export * from "./spec"; diff --git a/libs/auth/src/common/models/spec/fake-user-decryption-options.ts b/libs/auth/src/common/models/spec/fake-user-decryption-options.ts new file mode 100644 index 00000000000..fe4a1203c67 --- /dev/null +++ b/libs/auth/src/common/models/spec/fake-user-decryption-options.ts @@ -0,0 +1,38 @@ +import { + KeyConnectorUserDecryptionOption, + TrustedDeviceUserDecryptionOption, + UserDecryptionOptions, +} from "../domain"; + +// To discourage creating new user decryption options, we don't expose a constructor. +// These helpers are for testing purposes only. + +/** Testing helper for creating new instances of `UserDecryptionOptions` */ +export class FakeUserDecryptionOptions extends UserDecryptionOptions { + constructor(init: Partial) { + super(); + Object.assign(this, init); + } +} + +/** Testing helper for creating new instances of `KeyConnectorUserDecryptionOption` */ +export class FakeKeyConnectorUserDecryptionOption extends KeyConnectorUserDecryptionOption { + constructor(keyConnectorUrl: string) { + super(); + this.keyConnectorUrl = keyConnectorUrl; + } +} + +/** Testing helper for creating new instances of `TrustedDeviceUserDecryptionOption` */ +export class FakeTrustedDeviceUserDecryptionOption extends TrustedDeviceUserDecryptionOption { + constructor( + hasAdminApproval: boolean, + hasLoginApprovingDevice: boolean, + hasManageResetPasswordPermission: boolean, + ) { + super(); + this.hasAdminApproval = hasAdminApproval; + this.hasLoginApprovingDevice = hasLoginApprovingDevice; + this.hasManageResetPasswordPermission = hasManageResetPasswordPermission; + } +} diff --git a/libs/auth/src/common/models/spec/index.ts b/libs/auth/src/common/models/spec/index.ts new file mode 100644 index 00000000000..d05bc2bf2c8 --- /dev/null +++ b/libs/auth/src/common/models/spec/index.ts @@ -0,0 +1 @@ +export * from "./fake-user-decryption-options"; diff --git a/libs/auth/src/common/services/index.ts b/libs/auth/src/common/services/index.ts index 2b131c7afc2..12215cf6b4d 100644 --- a/libs/auth/src/common/services/index.ts +++ b/libs/auth/src/common/services/index.ts @@ -1,3 +1,4 @@ export * from "./pin-crypto/pin-crypto.service.implementation"; export * from "./login-strategies/login-strategy.service"; +export * from "./user-decryption-options/user-decryption-options.service"; export * from "./auth-request/auth-request.service"; diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts index 3d4c1b7b7d5..981e4d81ac7 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.spec.ts @@ -25,8 +25,12 @@ import { KdfType } from "@bitwarden/common/platform/enums"; import { FakeGlobalState, FakeGlobalStateProvider } from "@bitwarden/common/spec"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; -import { AuthRequestServiceAbstraction } from "../../abstractions"; +import { + AuthRequestServiceAbstraction, + InternalUserDecryptionOptionsServiceAbstraction, +} from "../../abstractions"; import { PasswordLoginCredentials } from "../../models"; +import { UserDecryptionOptionsService } from "../user-decryption-options/user-decryption-options.service"; import { LoginStrategyService } from "./login-strategy.service"; import { CACHE_EXPIRATION_KEY } from "./login-strategy.state"; @@ -51,6 +55,7 @@ describe("LoginStrategyService", () => { let policyService: MockProxy; let deviceTrustCryptoService: MockProxy; let authRequestService: MockProxy; + let userDecryptionOptionsService: MockProxy; let billingAccountProfileStateService: MockProxy; let stateProvider: FakeGlobalStateProvider; @@ -74,6 +79,7 @@ describe("LoginStrategyService", () => { policyService = mock(); deviceTrustCryptoService = mock(); authRequestService = mock(); + userDecryptionOptionsService = mock(); billingAccountProfileStateService = mock(); stateProvider = new FakeGlobalStateProvider(); @@ -95,6 +101,7 @@ describe("LoginStrategyService", () => { policyService, deviceTrustCryptoService, authRequestService, + userDecryptionOptionsService, stateProvider, billingAccountProfileStateService, ); diff --git a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts index 5c0e4140446..5dbc3397cf4 100644 --- a/libs/auth/src/common/services/login-strategies/login-strategy.service.ts +++ b/libs/auth/src/common/services/login-strategies/login-strategy.service.ts @@ -40,6 +40,7 @@ import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/pass import { MasterKey } from "@bitwarden/common/types/key"; import { AuthRequestServiceAbstraction, LoginStrategyServiceAbstraction } from "../../abstractions"; +import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; import { AuthRequestLoginStrategy } from "../../login-strategies/auth-request-login.strategy"; import { PasswordLoginStrategy } from "../../login-strategies/password-login.strategy"; import { SsoLoginStrategy } from "../../login-strategies/sso-login.strategy"; @@ -101,6 +102,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { protected policyService: PolicyService, protected deviceTrustCryptoService: DeviceTrustCryptoServiceAbstraction, protected authRequestService: AuthRequestServiceAbstraction, + protected userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction, protected stateProvider: GlobalStateProvider, protected billingAccountProfileStateService: BillingAccountProfileStateService, ) { @@ -354,6 +356,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.passwordStrengthService, this.policyService, this, @@ -371,6 +374,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.keyConnectorService, this.deviceTrustCryptoService, this.authRequestService, @@ -389,6 +393,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.environmentService, this.keyConnectorService, this.billingAccountProfileStateService, @@ -405,6 +410,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.deviceTrustCryptoService, this.billingAccountProfileStateService, ); @@ -420,6 +426,7 @@ export class LoginStrategyService implements LoginStrategyServiceAbstraction { this.logService, this.stateService, this.twoFactorService, + this.userDecryptionOptionsService, this.billingAccountProfileStateService, ); } diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts new file mode 100644 index 00000000000..e8bb1b38cee --- /dev/null +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.spec.ts @@ -0,0 +1,94 @@ +import { firstValueFrom } from "rxjs"; + +import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { Utils } from "@bitwarden/common/platform/misc/utils"; +import { + FakeAccountService, + FakeStateProvider, + mockAccountServiceWith, +} from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { + USER_DECRYPTION_OPTIONS, + UserDecryptionOptionsService, +} from "./user-decryption-options.service"; + +describe("UserDecryptionOptionsService", () => { + let sut: UserDecryptionOptionsService; + + const fakeUserId = Utils.newGuid() as UserId; + let fakeAccountService: FakeAccountService; + let fakeStateProvider: FakeStateProvider; + + beforeEach(() => { + fakeAccountService = mockAccountServiceWith(fakeUserId); + fakeStateProvider = new FakeStateProvider(fakeAccountService); + + sut = new UserDecryptionOptionsService(fakeStateProvider); + }); + + const userDecryptionOptions = { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }; + + describe("userDecryptionOptions$", () => { + it("should return the active user's decryption options", async () => { + await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions); + + const result = await firstValueFrom(sut.userDecryptionOptions$); + + expect(result).toEqual(userDecryptionOptions); + }); + }); + + describe("hasMasterPassword$", () => { + it("should return the hasMasterPassword property of the active user's decryption options", async () => { + await fakeStateProvider.setUserState(USER_DECRYPTION_OPTIONS, userDecryptionOptions); + + const result = await firstValueFrom(sut.hasMasterPassword$); + + expect(result).toBe(true); + }); + }); + + describe("userDecryptionOptionsById$", () => { + it("should return the user decryption options for the given user", async () => { + const givenUser = Utils.newGuid() as UserId; + await fakeAccountService.addAccount(givenUser, { + name: "Test User 1", + email: "test1@email.com", + status: AuthenticationStatus.Locked, + }); + await fakeStateProvider.setUserState( + USER_DECRYPTION_OPTIONS, + userDecryptionOptions, + givenUser, + ); + + const result = await firstValueFrom(sut.userDecryptionOptionsById$(givenUser)); + + expect(result).toEqual(userDecryptionOptions); + }); + }); + + describe("setUserDecryptionOptions", () => { + it("should set the active user's decryption options", async () => { + await sut.setUserDecryptionOptions(userDecryptionOptions); + + const result = await firstValueFrom( + fakeStateProvider.getActive(USER_DECRYPTION_OPTIONS).state$, + ); + + expect(result).toEqual(userDecryptionOptions); + }); + }); +}); diff --git a/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts new file mode 100644 index 00000000000..6651ffd9e51 --- /dev/null +++ b/libs/auth/src/common/services/user-decryption-options/user-decryption-options.service.ts @@ -0,0 +1,47 @@ +import { map } from "rxjs"; + +import { + ActiveUserState, + StateProvider, + USER_DECRYPTION_OPTIONS_DISK, + UserKeyDefinition, +} from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/src/types/guid"; + +import { InternalUserDecryptionOptionsServiceAbstraction } from "../../abstractions/user-decryption-options.service.abstraction"; +import { UserDecryptionOptions } from "../../models"; + +export const USER_DECRYPTION_OPTIONS = new UserKeyDefinition( + USER_DECRYPTION_OPTIONS_DISK, + "decryptionOptions", + { + deserializer: (decryptionOptions) => UserDecryptionOptions.fromJSON(decryptionOptions), + clearOn: ["logout"], + }, +); + +export class UserDecryptionOptionsService + implements InternalUserDecryptionOptionsServiceAbstraction +{ + private userDecryptionOptionsState: ActiveUserState; + + userDecryptionOptions$; + hasMasterPassword$; + + constructor(private stateProvider: StateProvider) { + this.userDecryptionOptionsState = this.stateProvider.getActive(USER_DECRYPTION_OPTIONS); + + this.userDecryptionOptions$ = this.userDecryptionOptionsState.state$; + this.hasMasterPassword$ = this.userDecryptionOptions$.pipe( + map((options) => options?.hasMasterPassword ?? false), + ); + } + + userDecryptionOptionsById$(userId: UserId) { + return this.stateProvider.getUser(userId, USER_DECRYPTION_OPTIONS).state$; + } + + async setUserDecryptionOptions(userDecryptionOptions: UserDecryptionOptions): Promise { + await this.userDecryptionOptionsState.update((_) => userDecryptionOptions); + } +} diff --git a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts index 5f37e7d956f..415355cfc77 100644 --- a/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts +++ b/libs/common/src/auth/abstractions/device-trust-crypto.service.abstraction.ts @@ -1,8 +1,11 @@ +import { Observable } from "rxjs"; + import { EncString } from "../../platform/models/domain/enc-string"; import { DeviceKey, UserKey } from "../../types/key"; import { DeviceResponse } from "../abstractions/devices/responses/device.response"; export abstract class DeviceTrustCryptoServiceAbstraction { + supportsDeviceTrust$: Observable; /** * @description Retrieves the users choice to trust the device which can only happen after decryption * Note: this value should only be used once and then reset @@ -20,6 +23,4 @@ export abstract class DeviceTrustCryptoServiceAbstraction { deviceKey?: DeviceKey, ) => Promise; rotateDevicesTrust: (newUserKey: UserKey, masterPasswordHash: string) => Promise; - - supportsDeviceTrust: () => Promise; } diff --git a/libs/common/src/auth/models/domain/auth-result.ts b/libs/common/src/auth/models/domain/auth-result.ts index 16f58c64593..993ce08d589 100644 --- a/libs/common/src/auth/models/domain/auth-result.ts +++ b/libs/common/src/auth/models/domain/auth-result.ts @@ -8,7 +8,7 @@ export class AuthResult { // TODO: PM-3287 - Remove this after 3 releases of backwards compatibility. - Target release 2023.12 for removal /** * @deprecated - * Replace with using AccountDecryptionOptions to determine if the user does + * Replace with using UserDecryptionOptions to determine if the user does * not have a master password and is not using Key Connector. * */ resetMasterPassword = false; diff --git a/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts b/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts deleted file mode 100644 index 3422c078e46..00000000000 --- a/libs/common/src/auth/models/domain/user-decryption-options/key-connector-user-decryption-option.ts +++ /dev/null @@ -1,3 +0,0 @@ -export class KeyConnectorUserDecryptionOption { - constructor(public keyConnectorUrl: string) {} -} diff --git a/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts b/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts deleted file mode 100644 index 6d2dc9d6334..00000000000 --- a/libs/common/src/auth/models/domain/user-decryption-options/trusted-device-user-decryption-option.ts +++ /dev/null @@ -1,7 +0,0 @@ -export class TrustedDeviceUserDecryptionOption { - constructor( - public hasAdminApproval: boolean, - public hasLoginApprovingDevice: boolean, - public hasManageResetPasswordPermission: boolean, - ) {} -} diff --git a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts index 9aa558ba537..71f83f07c3b 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.implementation.ts @@ -1,4 +1,6 @@ -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map, Observable } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; @@ -21,6 +23,8 @@ import { } from "../models/request/update-devices-trust.request"; export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstraction { + supportsDeviceTrust$: Observable; + constructor( private keyGenerationService: KeyGenerationService, private cryptoFunctionService: CryptoFunctionService, @@ -31,7 +35,12 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac private devicesApiService: DevicesApiServiceAbstraction, private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, - ) {} + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, + ) { + this.supportsDeviceTrust$ = this.userDecryptionOptionsService.userDecryptionOptions$.pipe( + map((options) => options?.trustedDeviceOption != null ?? false), + ); + } /** * @description Retrieves the users choice to trust the device which can only happen after decryption @@ -203,9 +212,4 @@ export class DeviceTrustCryptoService implements DeviceTrustCryptoServiceAbstrac return null; } } - - async supportsDeviceTrust(): Promise { - const decryptionOptions = await this.stateService.getAccountDecryptionOptions(); - return decryptionOptions?.trustedDeviceOption != null; - } } diff --git a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts index 8c0a62b125e..1d33223dddb 100644 --- a/libs/common/src/auth/services/device-trust-crypto.service.spec.ts +++ b/libs/common/src/auth/services/device-trust-crypto.service.spec.ts @@ -1,6 +1,9 @@ import { matches, mock } from "jest-mock-extended"; -import { of } from "rxjs"; +import { BehaviorSubject, of } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + +import { UserDecryptionOptions } from "../../../../auth/src/common/models/domain/user-decryption-options"; import { DeviceType } from "../../enums"; import { AppIdService } from "../../platform/abstractions/app-id.service"; import { CryptoFunctionService } from "../../platform/abstractions/crypto-function.service"; @@ -34,10 +37,16 @@ describe("deviceTrustCryptoService", () => { const devicesApiService = mock(); const i18nService = mock(); const platformUtilsService = mock(); + const userDecryptionOptionsService = mock(); + + const decryptionOptions = new BehaviorSubject(null); beforeEach(() => { jest.clearAllMocks(); + decryptionOptions.next({} as any); + userDecryptionOptionsService.userDecryptionOptions$ = decryptionOptions; + deviceTrustCryptoService = new DeviceTrustCryptoService( keyGenerationService, cryptoFunctionService, @@ -48,6 +57,7 @@ describe("deviceTrustCryptoService", () => { devicesApiService, i18nService, platformUtilsService, + userDecryptionOptionsService, ); }); diff --git a/libs/common/src/auth/services/user-verification/user-verification.service.ts b/libs/common/src/auth/services/user-verification/user-verification.service.ts index 431348c7fc9..03e267d9db5 100644 --- a/libs/common/src/auth/services/user-verification/user-verification.service.ts +++ b/libs/common/src/auth/services/user-verification/user-verification.service.ts @@ -1,3 +1,7 @@ +import { firstValueFrom } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + import { PinCryptoServiceAbstraction } from "../../../../../auth/src/common/abstractions/pin-crypto.service.abstraction"; import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../../abstractions/vault-timeout/vault-timeout-settings.service"; import { CryptoService } from "../../../platform/abstractions/crypto.service"; @@ -33,6 +37,7 @@ export class UserVerificationService implements UserVerificationServiceAbstracti private cryptoService: CryptoService, private i18nService: I18nService, private userVerificationApiService: UserVerificationApiServiceAbstraction, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private pinCryptoService: PinCryptoServiceAbstraction, private logService: LogService, private vaultTimeoutSettingsService: VaultTimeoutSettingsServiceAbstraction, @@ -135,7 +140,6 @@ export class UserVerificationService implements UserVerificationServiceAbstracti case VerificationType.MasterPassword: return this.verifyUserByMasterPassword(verification); case VerificationType.PIN: - return this.verifyUserByPIN(verification); break; case VerificationType.Biometrics: return this.verifyUserByBiometrics(); @@ -210,16 +214,19 @@ export class UserVerificationService implements UserVerificationServiceAbstracti * Note: This only checks the server, not the local state * @param userId The user id to check. If not provided, the current user is used * @returns True if the user has a master password + * @deprecated Use UserDecryptionOptionsService.hasMasterPassword$ instead */ async hasMasterPassword(userId?: string): Promise { - const decryptionOptions = await this.stateService.getAccountDecryptionOptions({ userId }); + if (userId) { + const decryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); - if (decryptionOptions?.hasMasterPassword != undefined) { - return decryptionOptions.hasMasterPassword; + if (decryptionOptions?.hasMasterPassword != undefined) { + return decryptionOptions.hasMasterPassword; + } } - - // TODO: PM-3518 - Left for backwards compatibility, remove after 2023.12.0 - return !(await this.stateService.getUsesKeyConnector({ userId })); + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } async hasMasterPasswordAndMasterKeyHash(userId?: string): Promise { diff --git a/libs/common/src/platform/abstractions/state.service.ts b/libs/common/src/platform/abstractions/state.service.ts index 3bed46e769a..e0228ee062c 100644 --- a/libs/common/src/platform/abstractions/state.service.ts +++ b/libs/common/src/platform/abstractions/state.service.ts @@ -18,7 +18,7 @@ import { CipherView } from "../../vault/models/view/cipher.view"; import { AddEditCipherInfo } from "../../vault/types/add-edit-cipher-info"; import { KdfType } from "../enums"; import { ServerConfigData } from "../models/data/server-config.data"; -import { Account, AccountDecryptionOptions } from "../models/domain/account"; +import { Account } 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"; @@ -180,13 +180,6 @@ export abstract class StateService { ) => Promise; getShouldTrustDevice: (options?: StorageOptions) => Promise; setShouldTrustDevice: (value: boolean, options?: StorageOptions) => Promise; - getAccountDecryptionOptions: ( - options?: StorageOptions, - ) => Promise; - setAccountDecryptionOptions: ( - value: AccountDecryptionOptions, - options?: StorageOptions, - ) => Promise; getEmail: (options?: StorageOptions) => Promise; setEmail: (value: string, options?: StorageOptions) => Promise; getEmailVerified: (options?: StorageOptions) => Promise; diff --git a/libs/common/src/platform/models/domain/account.ts b/libs/common/src/platform/models/domain/account.ts index 460ce25c15e..c8d903cfea3 100644 --- a/libs/common/src/platform/models/domain/account.ts +++ b/libs/common/src/platform/models/domain/account.ts @@ -2,9 +2,6 @@ import { Jsonify } from "type-fest"; import { AdminAuthRequestStorable } from "../../../auth/models/domain/admin-auth-req-storable"; import { ForceSetPasswordReason } from "../../../auth/models/domain/force-set-password-reason"; -import { KeyConnectorUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/key-connector-user-decryption-option"; -import { TrustedDeviceUserDecryptionOption } from "../../../auth/models/domain/user-decryption-options/trusted-device-user-decryption-option"; -import { IdentityTokenResponse } from "../../../auth/models/response/identity-token.response"; import { UriMatchStrategySetting } from "../../../models/domain/domain-service"; import { GeneratorOptions } from "../../../tools/generator/generator-options"; import { @@ -235,103 +232,12 @@ export class AccountTokens { } } -export class AccountDecryptionOptions { - hasMasterPassword: boolean; - trustedDeviceOption?: TrustedDeviceUserDecryptionOption; - keyConnectorOption?: KeyConnectorUserDecryptionOption; - - constructor(init?: Partial) { - if (init) { - Object.assign(this, init); - } - } - - // TODO: these nice getters don't work because the Account object is not properly being deserialized out of - // JSON (the Account static fromJSON method is not running) so these getters don't exist on the - // account decryptions options object when pulled out of state. This is a bug that needs to be fixed later on - // get hasTrustedDeviceOption(): boolean { - // return this.trustedDeviceOption !== null && this.trustedDeviceOption !== undefined; - // } - - // get hasKeyConnectorOption(): boolean { - // return this.keyConnectorOption !== null && this.keyConnectorOption !== undefined; - // } - - static fromResponse(response: IdentityTokenResponse): AccountDecryptionOptions { - if (response == null) { - return null; - } - - const accountDecryptionOptions = new AccountDecryptionOptions(); - - if (response.userDecryptionOptions) { - // If the response has userDecryptionOptions, this means it's on a post-TDE server version and can interrogate - // the new decryption options. - const responseOptions = response.userDecryptionOptions; - accountDecryptionOptions.hasMasterPassword = responseOptions.hasMasterPassword; - - if (responseOptions.trustedDeviceOption) { - accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption( - responseOptions.trustedDeviceOption.hasAdminApproval, - responseOptions.trustedDeviceOption.hasLoginApprovingDevice, - responseOptions.trustedDeviceOption.hasManageResetPasswordPermission, - ); - } - - if (responseOptions.keyConnectorOption) { - accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( - responseOptions.keyConnectorOption.keyConnectorUrl, - ); - } - } else { - // If the response does not have userDecryptionOptions, this means it's on a pre-TDE server version and so - // we must base our decryption options on the presence of the keyConnectorUrl. - // Note that the presence of keyConnectorUrl implies that the user does not have a master password, as in pre-TDE - // server versions, a master password short-circuited the addition of the keyConnectorUrl to the response. - // TODO: remove this check after 2023.10 release (https://bitwarden.atlassian.net/browse/PM-3537) - const usingKeyConnector = response.keyConnectorUrl != null; - accountDecryptionOptions.hasMasterPassword = !usingKeyConnector; - if (usingKeyConnector) { - accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( - response.keyConnectorUrl, - ); - } - } - return accountDecryptionOptions; - } - - static fromJSON(obj: Jsonify): AccountDecryptionOptions { - if (obj == null) { - return null; - } - - const accountDecryptionOptions = Object.assign(new AccountDecryptionOptions(), obj); - - if (obj.trustedDeviceOption) { - accountDecryptionOptions.trustedDeviceOption = new TrustedDeviceUserDecryptionOption( - obj.trustedDeviceOption.hasAdminApproval, - obj.trustedDeviceOption.hasLoginApprovingDevice, - obj.trustedDeviceOption.hasManageResetPasswordPermission, - ); - } - - if (obj.keyConnectorOption) { - accountDecryptionOptions.keyConnectorOption = new KeyConnectorUserDecryptionOption( - obj.keyConnectorOption.keyConnectorUrl, - ); - } - - return accountDecryptionOptions; - } -} - export class Account { data?: AccountData = new AccountData(); keys?: AccountKeys = new AccountKeys(); profile?: AccountProfile = new AccountProfile(); settings?: AccountSettings = new AccountSettings(); tokens?: AccountTokens = new AccountTokens(); - decryptionOptions?: AccountDecryptionOptions = new AccountDecryptionOptions(); adminAuthRequest?: Jsonify = null; constructor(init: Partial) { @@ -356,10 +262,6 @@ export class Account { ...new AccountTokens(), ...init?.tokens, }, - decryptionOptions: { - ...new AccountDecryptionOptions(), - ...init?.decryptionOptions, - }, adminAuthRequest: init?.adminAuthRequest, }); } @@ -375,7 +277,6 @@ export class Account { profile: AccountProfile.fromJSON(json?.profile), settings: AccountSettings.fromJSON(json?.settings), tokens: AccountTokens.fromJSON(json?.tokens), - decryptionOptions: AccountDecryptionOptions.fromJSON(json?.decryptionOptions), adminAuthRequest: AdminAuthRequestStorable.fromJSON(json?.adminAuthRequest), }); } diff --git a/libs/common/src/platform/services/state.service.ts b/libs/common/src/platform/services/state.service.ts index 0985c9949a5..fc548b562ea 100644 --- a/libs/common/src/platform/services/state.service.ts +++ b/libs/common/src/platform/services/state.service.ts @@ -34,12 +34,7 @@ import { HtmlStorageLocation, KdfType, StorageLocation } from "../enums"; import { StateFactory } from "../factories/state-factory"; import { Utils } from "../misc/utils"; import { ServerConfigData } from "../models/data/server-config.data"; -import { - Account, - AccountData, - AccountDecryptionOptions, - AccountSettings, -} from "../models/domain/account"; +import { Account, AccountData, AccountSettings } from "../models/domain/account"; import { EncString } from "../models/domain/enc-string"; import { GlobalState } from "../models/domain/global-state"; import { State } from "../models/domain/state"; @@ -817,37 +812,6 @@ export class StateService< await this.saveAccount(account, options); } - async getAccountDecryptionOptions( - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return null; - } - - const account = await this.getAccount(options); - - return account?.decryptionOptions as AccountDecryptionOptions; - } - - async setAccountDecryptionOptions( - value: AccountDecryptionOptions, - options?: StorageOptions, - ): Promise { - options = this.reconcileOptions(options, await this.defaultOnDiskLocalOptions()); - - if (options?.userId == null) { - return; - } - - const account = await this.getAccount(options); - - account.decryptionOptions = value; - - await this.saveAccount(account, options); - } - async getEmail(options?: StorageOptions): Promise { return ( await this.getAccount(this.reconcileOptions(options, await this.defaultInMemoryOptions())) diff --git a/libs/common/src/platform/state/state-definitions.ts b/libs/common/src/platform/state/state-definitions.ts index fdd9277c7b5..3e6145cd5b5 100644 --- a/libs/common/src/platform/state/state-definitions.ts +++ b/libs/common/src/platform/state/state-definitions.ts @@ -44,6 +44,7 @@ export const TOKEN_DISK_LOCAL = new StateDefinition("tokenDiskLocal", "disk", { }); export const TOKEN_MEMORY = new StateDefinition("token", "memory"); export const LOGIN_STRATEGY_MEMORY = new StateDefinition("loginStrategy", "memory"); +export const USER_DECRYPTION_OPTIONS_DISK = new StateDefinition("userDecryptionOptions", "disk"); // Autofill diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts index 7eee4567754..4ce0159121c 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.spec.ts @@ -1,5 +1,10 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { firstValueFrom, of } from "rxjs"; +import { BehaviorSubject, firstValueFrom, map, of } from "rxjs"; + +import { + FakeUserDecryptionOptions as UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { Policy } from "../../admin-console/models/domain/policy"; @@ -8,12 +13,12 @@ import { VaultTimeoutAction } from "../../enums/vault-timeout-action.enum"; import { CryptoService } from "../../platform/abstractions/crypto.service"; import { StateService } from "../../platform/abstractions/state.service"; import { BiometricStateService } from "../../platform/biometrics/biometric-state.service"; -import { AccountDecryptionOptions } from "../../platform/models/domain/account"; import { EncString } from "../../platform/models/domain/enc-string"; import { VaultTimeoutSettingsService } from "./vault-timeout-settings.service"; describe("VaultTimeoutSettingsService", () => { + let userDecryptionOptionsService: MockProxy; let cryptoService: MockProxy; let tokenService: MockProxy; let policyService: MockProxy; @@ -21,12 +26,26 @@ describe("VaultTimeoutSettingsService", () => { const biometricStateService = mock(); let service: VaultTimeoutSettingsService; + let userDecryptionOptionsSubject: BehaviorSubject; + beforeEach(() => { + userDecryptionOptionsService = mock(); cryptoService = mock(); tokenService = mock(); policyService = mock(); stateService = mock(); + + userDecryptionOptionsSubject = new BehaviorSubject(null); + userDecryptionOptionsService.userDecryptionOptions$ = userDecryptionOptionsSubject; + userDecryptionOptionsService.hasMasterPassword$ = userDecryptionOptionsSubject.pipe( + map((options) => options?.hasMasterPassword ?? false), + ); + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + userDecryptionOptionsSubject, + ); + service = new VaultTimeoutSettingsService( + userDecryptionOptionsService, cryptoService, tokenService, policyService, @@ -49,9 +68,7 @@ describe("VaultTimeoutSettingsService", () => { }); it("contains Lock when the user has a master password", async () => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: true }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); const result = await firstValueFrom(service.availableVaultTimeoutActions$()); @@ -83,9 +100,7 @@ describe("VaultTimeoutSettingsService", () => { }); it("not contains Lock when the user does not have a master password, PIN, or biometrics", async () => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: false }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: false })); stateService.getPinKeyEncryptedUserKey.mockResolvedValue(null); stateService.getProtectedPin.mockResolvedValue(null); biometricStateService.biometricUnlockEnabled$ = of(false); @@ -107,9 +122,7 @@ describe("VaultTimeoutSettingsService", () => { `( "returns $expected when policy is $policy, and user preference is $userPreference", async ({ policy, userPreference, expected }) => { - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: true }), - ); + userDecryptionOptionsSubject.next(new UserDecryptionOptions({ hasMasterPassword: true })); policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), ); @@ -136,8 +149,8 @@ describe("VaultTimeoutSettingsService", () => { "returns $expected when policy is $policy, has unlock method is $unlockMethod, and user preference is $userPreference", async ({ unlockMethod, policy, userPreference, expected }) => { biometricStateService.biometricUnlockEnabled$ = of(unlockMethod); - stateService.getAccountDecryptionOptions.mockResolvedValue( - new AccountDecryptionOptions({ hasMasterPassword: false }), + userDecryptionOptionsSubject.next( + new UserDecryptionOptions({ hasMasterPassword: false }), ); policyService.getAll$.mockReturnValue( of(policy === null ? [] : ([{ data: { action: policy } }] as unknown as Policy[])), diff --git a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts index e8897d82b7d..4eb9e776992 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout-settings.service.ts @@ -1,5 +1,7 @@ import { defer, firstValueFrom } from "rxjs"; +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + import { VaultTimeoutSettingsService as VaultTimeoutSettingsServiceAbstraction } from "../../abstractions/vault-timeout/vault-timeout-settings.service"; import { PolicyService } from "../../admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "../../admin-console/enums"; @@ -19,6 +21,7 @@ export type PinLockType = "DISABLED" | "PERSISTANT" | "TRANSIENT"; export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceAbstraction { constructor( + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private cryptoService: CryptoService, private tokenService: TokenService, private policyService: PolicyService, @@ -174,12 +177,15 @@ export class VaultTimeoutSettingsService implements VaultTimeoutSettingsServiceA } private async userHasMasterPassword(userId: string): Promise { - const acctDecryptionOpts = await this.stateService.getAccountDecryptionOptions({ - userId: userId, - }); + if (userId) { + const decryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + ); - if (acctDecryptionOpts?.hasMasterPassword != undefined) { - return acctDecryptionOpts.hasMasterPassword; + if (decryptionOptions?.hasMasterPassword != undefined) { + return decryptionOptions.hasMasterPassword; + } } + return await firstValueFrom(this.userDecryptionOptionsService.hasMasterPassword$); } } diff --git a/libs/common/src/state-migrations/migrate.ts b/libs/common/src/state-migrations/migrate.ts index 1e3b925c2b2..6c2a3084667 100644 --- a/libs/common/src/state-migrations/migrate.ts +++ b/libs/common/src/state-migrations/migrate.ts @@ -39,6 +39,7 @@ import { OrganizationMigrator } from "./migrations/40-move-organization-state-to import { EventCollectionMigrator } from "./migrations/41-move-event-collection-to-state-provider"; import { EnableFaviconMigrator } from "./migrations/42-move-enable-favicon-to-domain-settings-state-provider"; import { AutoConfirmFingerPrintsMigrator } from "./migrations/43-move-auto-confirm-finger-prints-to-state-provider"; +import { UserDecryptionOptionsMigrator } from "./migrations/44-move-user-decryption-options-to-state-provider"; import { AddKeyTypeToOrgKeysMigrator } from "./migrations/5-add-key-type-to-org-keys"; import { RemoveLegacyEtmKeyMigrator } from "./migrations/6-remove-legacy-etm-key"; import { MoveBiometricAutoPromptToAccount } from "./migrations/7-move-biometric-auto-prompt-to-account"; @@ -47,7 +48,7 @@ import { MoveBrowserSettingsToGlobal } from "./migrations/9-move-browser-setting import { MinVersionMigrator } from "./migrations/min-version"; export const MIN_VERSION = 3; -export const CURRENT_VERSION = 43; +export const CURRENT_VERSION = 44; export type MinVersion = typeof MIN_VERSION; export function createMigrationBuilder() { @@ -92,7 +93,8 @@ export function createMigrationBuilder() { .with(OrganizationMigrator, 39, 40) .with(EventCollectionMigrator, 40, 41) .with(EnableFaviconMigrator, 41, 42) - .with(AutoConfirmFingerPrintsMigrator, 42, CURRENT_VERSION); + .with(AutoConfirmFingerPrintsMigrator, 42, 43) + .with(UserDecryptionOptionsMigrator, 43, CURRENT_VERSION); } export async function currentVersion( diff --git a/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts new file mode 100644 index 00000000000..90254f1c435 --- /dev/null +++ b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.spec.ts @@ -0,0 +1,238 @@ +import { any, MockProxy } from "jest-mock-extended"; + +import { MigrationHelper } from "../migration-helper"; +import { mockMigrationHelper } from "../migration-helper.spec"; + +import { UserDecryptionOptionsMigrator } from "./44-move-user-decryption-options-to-state-provider"; + +function exampleJSON() { + return { + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + decryptionOptions: { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }, + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + decryptionOptions: { + hasMasterPassword: false, + trustedDeviceOption: { + hasAdminApproval: true, + hasLoginApprovingDevice: true, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://selfhosted.bitwarden.com", + }, + }, + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +function rollbackJSON() { + return { + user_FirstAccount_decryptionOptions_userDecryptionOptions: { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }, + user_SecondAccount_decryptionOptions_userDecryptionOptions: { + hasMasterPassword: false, + trustedDeviceOption: { + hasAdminApproval: true, + hasLoginApprovingDevice: true, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://selfhosted.bitwarden.com", + }, + }, + user_ThirdAccount_decryptionOptions_userDecryptionOptions: {}, + global: { + otherStuff: "otherStuff1", + }, + authenticatedAccounts: ["FirstAccount", "SecondAccount", "ThirdAccount"], + FirstAccount: { + decryptionOptions: { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }, + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }, + SecondAccount: { + decryptionOptions: { + hasMasterPassword: false, + trustedDeviceOption: { + hasAdminApproval: true, + hasLoginApprovingDevice: true, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://selfhosted.bitwarden.com", + }, + }, + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }, + }; +} + +describe("UserDecryptionOptionsMigrator", () => { + let helper: MockProxy; + let sut: UserDecryptionOptionsMigrator; + const keyDefinitionLike = { + key: "decryptionOptions", + stateDefinition: { + name: "userDecryptionOptions", + }, + }; + + describe("migrate", () => { + beforeEach(() => { + helper = mockMigrationHelper(exampleJSON(), 43); + sut = new UserDecryptionOptionsMigrator(43, 44); + }); + + it("should remove decryptionOptions from all accounts", async () => { + await sut.migrate(helper); + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should set decryptionOptions provider value for each account", async () => { + await sut.migrate(helper); + + expect(helper.setToUser).toHaveBeenCalledWith("FirstAccount", keyDefinitionLike, { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }); + + expect(helper.setToUser).toHaveBeenCalledWith("SecondAccount", keyDefinitionLike, { + hasMasterPassword: false, + trustedDeviceOption: { + hasAdminApproval: true, + hasLoginApprovingDevice: true, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://selfhosted.bitwarden.com", + }, + }); + }); + }); + + describe("rollback", () => { + beforeEach(() => { + helper = mockMigrationHelper(rollbackJSON(), 44); + sut = new UserDecryptionOptionsMigrator(43, 44); + }); + + it.each(["FirstAccount", "SecondAccount", "ThirdAccount"])( + "should null out new values", + async (userId) => { + await sut.rollback(helper); + + expect(helper.setToUser).toHaveBeenCalledWith(userId, keyDefinitionLike, null); + }, + ); + + it("should add explicit value back to accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).toHaveBeenCalledWith("FirstAccount", { + decryptionOptions: { + hasMasterPassword: true, + trustedDeviceOption: { + hasAdminApproval: false, + hasLoginApprovingDevice: false, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://keyconnector.bitwarden.com", + }, + }, + profile: { + otherStuff: "overStuff2", + }, + otherStuff: "otherStuff3", + }); + expect(helper.set).toHaveBeenCalledWith("SecondAccount", { + decryptionOptions: { + hasMasterPassword: false, + trustedDeviceOption: { + hasAdminApproval: true, + hasLoginApprovingDevice: true, + hasManageResetPasswordPermission: true, + }, + keyConnectorOption: { + keyConnectorUrl: "https://selfhosted.bitwarden.com", + }, + }, + profile: { + otherStuff: "otherStuff4", + }, + otherStuff: "otherStuff5", + }); + }); + + it("should not try to restore values to missing accounts", async () => { + await sut.rollback(helper); + + expect(helper.set).not.toHaveBeenCalledWith("ThirdAccount", any()); + }); + }); +}); diff --git a/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts new file mode 100644 index 00000000000..708b096280c --- /dev/null +++ b/libs/common/src/state-migrations/migrations/44-move-user-decryption-options-to-state-provider.ts @@ -0,0 +1,57 @@ +import { KeyDefinitionLike, MigrationHelper } from "../migration-helper"; +import { Migrator } from "../migrator"; + +type DecryptionOptionsType = { + hasMasterPassword: boolean; + trustedDeviceOption?: { + hasAdminApproval: boolean; + hasLoginApprovingDevice: boolean; + hasManageResetPasswordPermission: boolean; + }; + keyConnectorOption?: { + keyConnectorUrl: string; + }; +}; + +type ExpectedAccountType = { + decryptionOptions?: DecryptionOptionsType; +}; + +const USER_DECRYPTION_OPTIONS: KeyDefinitionLike = { + key: "decryptionOptions", + stateDefinition: { + name: "userDecryptionOptions", + }, +}; + +export class UserDecryptionOptionsMigrator extends Migrator<43, 44> { + async migrate(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function migrateAccount(userId: string, account: ExpectedAccountType): Promise { + const value = account?.decryptionOptions; + if (value != null) { + await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, value); + delete account.decryptionOptions; + await helper.set(userId, account); + } + } + + await Promise.all([...accounts.map(({ userId, account }) => migrateAccount(userId, account))]); + } + async rollback(helper: MigrationHelper): Promise { + const accounts = await helper.getAccounts(); + async function rollbackAccount(userId: string, account: ExpectedAccountType): Promise { + const value: DecryptionOptionsType = await helper.getFromUser( + userId, + USER_DECRYPTION_OPTIONS, + ); + if (account) { + account.decryptionOptions = Object.assign(account.decryptionOptions, value); + await helper.set(userId, account); + } + await helper.setToUser(userId, USER_DECRYPTION_OPTIONS, null); + } + + await Promise.all([...accounts.map(({ userId, account }) => rollbackAccount(userId, account))]); + } +} diff --git a/libs/common/src/vault/services/sync/sync.service.ts b/libs/common/src/vault/services/sync/sync.service.ts index 654b1137f5d..3e8bd92a7ac 100644 --- a/libs/common/src/vault/services/sync/sync.service.ts +++ b/libs/common/src/vault/services/sync/sync.service.ts @@ -1,3 +1,7 @@ +import { firstValueFrom } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; + import { ApiService } from "../../../abstractions/api.service"; import { InternalOrganizationServiceAbstraction } from "../../../admin-console/abstractions/organization/organization.service.abstraction"; import { InternalPolicyService } from "../../../admin-console/abstractions/policy/policy.service.abstraction"; @@ -24,7 +28,6 @@ import { LogService } from "../../../platform/abstractions/log.service"; import { MessagingService } from "../../../platform/abstractions/messaging.service"; import { StateService } from "../../../platform/abstractions/state.service"; import { sequentialize } from "../../../platform/misc/sequentialize"; -import { AccountDecryptionOptions } from "../../../platform/models/domain/account"; import { SendData } from "../../../tools/send/models/data/send.data"; import { SendResponse } from "../../../tools/send/models/response/send.response"; import { SendApiService } from "../../../tools/send/services/send-api.service.abstraction"; @@ -62,6 +65,7 @@ export class SyncService implements SyncServiceAbstraction { private folderApiService: FolderApiServiceAbstraction, private organizationService: InternalOrganizationServiceAbstraction, private sendApiService: SendApiService, + private userDecryptionOptionsService: UserDecryptionOptionsServiceAbstraction, private avatarService: AvatarService, private logoutCallback: (expired: boolean) => Promise, private billingAccountProfileStateService: BillingAccountProfileStateService, @@ -353,19 +357,12 @@ export class SyncService implements SyncServiceAbstraction { ); } - const acctDecryptionOpts: AccountDecryptionOptions = - await this.stateService.getAccountDecryptionOptions(); + const userDecryptionOptions = await firstValueFrom( + this.userDecryptionOptionsService.userDecryptionOptions$, + ); - // Account decryption options should never be null or undefined b/c it is always initialized - // during the processing of the ID token response, but there might be a state issue - // where it is being overwritten with undefined affecting browser extension + FireFox users. - // TODO: Consider removing this once we figure out the root cause of the state issue or after the state provider refactor. - if (acctDecryptionOpts === null || acctDecryptionOpts === undefined) { + if (userDecryptionOptions === null || userDecryptionOptions === undefined) { this.logService.error("Sync: Account decryption options are null or undefined."); - // Early return as a bandaid to allow the rest of the sync to continue so users can access - // their data that they might have added from another device. - // Otherwise, trying to access properties on undefined below will throw an error. - return; } // Even though TDE users should only be in a single org (per single org policy), check @@ -384,8 +381,8 @@ export class SyncService implements SyncServiceAbstraction { } if ( - acctDecryptionOpts.trustedDeviceOption !== undefined && - !acctDecryptionOpts.hasMasterPassword && + userDecryptionOptions.trustedDeviceOption !== undefined && + !userDecryptionOptions.hasMasterPassword && hasManageResetPasswordPermission ) { // TDE user w/out MP went from having no password reset permission to having it. From 0bc624fd508e9d66e7d3adda26b212d8fa06467c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:24:03 +0100 Subject: [PATCH 14/17] [deps] Platform: Update Rust crate base64 to v0.22.0 (#8188) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel GarcĂ­a --- apps/desktop/desktop_native/Cargo.lock | 4 ++-- apps/desktop/desktop_native/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/desktop/desktop_native/Cargo.lock b/apps/desktop/desktop_native/Cargo.lock index 138394886d3..89170f4cc38 100644 --- a/apps/desktop/desktop_native/Cargo.lock +++ b/apps/desktop/desktop_native/Cargo.lock @@ -84,9 +84,9 @@ dependencies = [ [[package]] name = "base64" -version = "0.21.5" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" diff --git a/apps/desktop/desktop_native/Cargo.toml b/apps/desktop/desktop_native/Cargo.toml index 2542e4d214a..48536934eea 100644 --- a/apps/desktop/desktop_native/Cargo.toml +++ b/apps/desktop/desktop_native/Cargo.toml @@ -16,7 +16,7 @@ manual_test = [] aes = "=0.8.4" anyhow = "=1.0.80" arboard = { version = "=3.3.0", default-features = false, features = ["wayland-data-control"] } -base64 = "=0.21.5" +base64 = "=0.22.0" cbc = { version = "=0.1.2", features = ["alloc"] } napi = { version = "=2.13.3", features = ["async"] } napi-derive = "=2.13.0" From e80ee2ec55e88ef23f48cbc02a279d479ba23a4d Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Thu, 21 Mar 2024 12:30:01 +0100 Subject: [PATCH 15/17] [PM-6195] Refactor injection of services in browser services module (#8405) * Remove PasswordStrengthService from popup/services.module * Remove ImportService from popup/services.module --------- Co-authored-by: Daniel James Smith --- apps/browser/src/popup/services/services.module.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 52de0303fae..5e080adf16b 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -82,7 +82,6 @@ import { import { SearchService } from "@bitwarden/common/services/search.service"; import { PasswordGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/password"; import { UsernameGenerationServiceAbstraction } from "@bitwarden/common/tools/generator/username"; -import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service"; import { SendApiService as SendApiServiceAbstraction } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { @@ -96,7 +95,6 @@ import { FolderService as FolderServiceAbstraction } from "@bitwarden/common/vau import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { DialogService } from "@bitwarden/components"; -import { ImportServiceAbstraction } from "@bitwarden/importer/core"; import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core"; import { UnauthGuardService } from "../../auth/popup/services"; @@ -292,11 +290,6 @@ function getBgService(service: keyof MainBackground) { }, deps: [DomSanitizer, ToastrService], }, - { - provide: PasswordStrengthServiceAbstraction, - useFactory: getBgService("passwordStrengthService"), - deps: [], - }, { provide: PasswordGenerationServiceAbstraction, useFactory: getBgService("passwordGenerationService"), @@ -350,11 +343,6 @@ function getBgService(service: keyof MainBackground) { useFactory: getBgService("autofillService"), deps: [], }, - { - provide: ImportServiceAbstraction, - useFactory: getBgService("importService"), - deps: [], - }, { provide: VaultExportServiceAbstraction, useFactory: getBgService("exportService"), From cd5dc09d25e901c59eba9ec4a68858801fcee267 Mon Sep 17 00:00:00 2001 From: Prithvi Reddy Date: Thu, 21 Mar 2024 19:13:29 +0530 Subject: [PATCH 16/17] [PM-3316] Feature addition - Toggle Hardware Acceleration [Desktop] (#5968) Added a toggle for disabling/enabling hardware acceleration on Desktop client. Resolves #2615 --------- Co-authored-by: Hinton --- .../src/app/accounts/settings.component.html | 17 ++++++++++++ .../src/app/accounts/settings.component.ts | 12 +++++++++ .../src/app/services/services.module.ts | 6 +++++ apps/desktop/src/locales/en/messages.json | 6 +++++ apps/desktop/src/main.ts | 16 ++++++++++++ .../services/desktop-settings.service.ts | 26 +++++++++++++++++++ .../src/platform/state/state-definitions.ts | 1 + 7 files changed, 84 insertions(+) create mode 100644 apps/desktop/src/platform/services/desktop-settings.service.ts diff --git a/apps/desktop/src/app/accounts/settings.component.html b/apps/desktop/src/app/accounts/settings.component.html index 7a61a55ccff..b35d3ed2254 100644 --- a/apps/desktop/src/app/accounts/settings.component.html +++ b/apps/desktop/src/app/accounts/settings.component.html @@ -412,6 +412,23 @@ "enableBrowserIntegrationFingerprintDesc" | i18n }}
+
+
+ +
+ {{ + "enableHardwareAccelerationDesc" | i18n + }} +