From dab60dbaea503c91840b847871a0cc2d734e6899 Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 12:58:00 -0700 Subject: [PATCH 1/8] [PM-11926] - send created redirect (#11140) * send created redirect * fix test * fix test * fix send form save * return SendData from saveSend * When saving a Send, bubble up a SendView which can be passed to the SendCreated component * Use events to initiate navigation and move actual navigation into client-specific component --------- Co-authored-by: Daniel James Smith --- .../add-edit/send-add-edit.component.html | 3 +- .../add-edit/send-add-edit.component.ts | 18 +++++++++-- .../send-created/send-created.component.html | 7 ++++- .../send-created.component.spec.ts | 11 ++++--- .../send-created/send-created.component.ts | 12 ++++--- .../services/send-api.service.abstraction.ts | 2 +- .../tools/send/services/send-api.service.ts | 3 +- .../components/send-form.component.ts | 31 ++++++++++++------- .../services/default-send-form.service.ts | 3 +- 9 files changed, 62 insertions(+), 28 deletions(-) diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html index b3783bfed3a..40c942539f6 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.html @@ -4,7 +4,8 @@ diff --git a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts index c84b9717df1..20b472f97f3 100644 --- a/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts +++ b/apps/browser/src/tools/popup/send-v2/add-edit/send-add-edit.component.ts @@ -2,12 +2,13 @@ import { CommonModule, Location } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; -import { ActivatedRoute, Params } from "@angular/router"; +import { ActivatedRoute, Params, Router } from "@angular/router"; import { map, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { SendType } from "@bitwarden/common/tools/send/enums/send-type"; +import { SendView } from "@bitwarden/common/tools/send/models/view/send.view"; import { SendApiService } from "@bitwarden/common/tools/send/services/send-api.service.abstraction"; import { SendId } from "@bitwarden/common/types/guid"; import { @@ -95,14 +96,25 @@ export class SendAddEditComponent { private sendApiService: SendApiService, private toastService: ToastService, private dialogService: DialogService, + private router: Router, ) { this.subscribeToParams(); } /** - * Handles the event when the send is saved. + * Handles the event when the send is created. */ - onSendSaved() { + async onSendCreated(send: SendView) { + await this.router.navigate(["/send-created"], { + queryParams: { sendId: send.id }, + }); + return; + } + + /** + * Handles the event when the send is updated. + */ + onSendUpdated(send: SendView) { this.location.back(); } diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html index 9b56fa74d91..c97d3da1396 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.html @@ -1,6 +1,11 @@
- + diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts index 413f22565e1..bcc4d2e2ccb 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.spec.ts @@ -1,6 +1,6 @@ import { CommonModule, Location } from "@angular/common"; import { ComponentFixture, TestBed } from "@angular/core/testing"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink } from "@angular/router"; import { RouterTestingModule } from "@angular/router/testing"; import { MockProxy, mock } from "jest-mock-extended"; import { of } from "rxjs"; @@ -33,6 +33,7 @@ describe("SendCreatedComponent", () => { let location: MockProxy; let activatedRoute: MockProxy; let environmentService: MockProxy; + let router: MockProxy; const sendId = "test-send-id"; const deletionDate = new Date(); @@ -52,6 +53,7 @@ describe("SendCreatedComponent", () => { location = mock(); activatedRoute = mock(); environmentService = mock(); + router = mock(); Object.defineProperty(environmentService, "environment$", { configurable: true, get: () => of(new SelfHostedEnvironment({ webVault: "https://example.com" })), @@ -89,6 +91,7 @@ describe("SendCreatedComponent", () => { { provide: ConfigService, useValue: mock() }, { provide: EnvironmentService, useValue: environmentService }, { provide: PopupRouterCacheService, useValue: mock() }, + { provide: Router, useValue: router }, ], }).compileComponents(); }); @@ -109,10 +112,10 @@ describe("SendCreatedComponent", () => { expect(component["daysAvailable"]).toBe(7); }); - it("should navigate back on close", () => { + it("should navigate back to send list on close", async () => { fixture.detectChanges(); - component.close(); - expect(location.back).toHaveBeenCalled(); + await component.close(); + expect(router.navigate).toHaveBeenCalledWith(["/tabs/send"]); }); describe("getDaysAvailable", () => { diff --git a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts index 92339774d05..4ed4da2f81d 100644 --- a/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts +++ b/apps/browser/src/tools/popup/send-v2/send-created/send-created.component.ts @@ -1,7 +1,7 @@ -import { CommonModule, Location } from "@angular/common"; +import { CommonModule } from "@angular/common"; import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { ActivatedRoute, RouterLink } from "@angular/router"; +import { ActivatedRoute, Router, RouterLink, RouterModule } from "@angular/router"; import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -30,6 +30,7 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page PopupHeaderComponent, PopupPageComponent, RouterLink, + RouterModule, PopupFooterComponent, IconModule, ], @@ -45,10 +46,11 @@ export class SendCreatedComponent { private sendService: SendService, private route: ActivatedRoute, private toastService: ToastService, - private location: Location, + private router: Router, private environmentService: EnvironmentService, ) { const sendId = this.route.snapshot.queryParamMap.get("sendId"); + this.sendService.sendViews$.pipe(takeUntilDestroyed()).subscribe((sendViews) => { this.send = sendViews.find((s) => s.id === sendId); if (this.send) { @@ -62,8 +64,8 @@ export class SendCreatedComponent { return Math.max(0, Math.ceil((send.deletionDate.getTime() - now) / (1000 * 60 * 60 * 24))); } - close() { - this.location.back(); + async close() { + await this.router.navigate(["/tabs/send"]); } async copyLink() { diff --git a/libs/common/src/tools/send/services/send-api.service.abstraction.ts b/libs/common/src/tools/send/services/send-api.service.abstraction.ts index 100985c4870..4109df19680 100644 --- a/libs/common/src/tools/send/services/send-api.service.abstraction.ts +++ b/libs/common/src/tools/send/services/send-api.service.abstraction.ts @@ -36,5 +36,5 @@ export abstract class SendApiService { renewSendFileUploadUrl: (sendId: string, fileId: string) => Promise; removePassword: (id: string) => Promise; delete: (id: string) => Promise; - save: (sendData: [Send, EncArrayBuffer]) => Promise; + save: (sendData: [Send, EncArrayBuffer]) => Promise; } diff --git a/libs/common/src/tools/send/services/send-api.service.ts b/libs/common/src/tools/send/services/send-api.service.ts index 2cb2ff1c2f0..ff71408bce3 100644 --- a/libs/common/src/tools/send/services/send-api.service.ts +++ b/libs/common/src/tools/send/services/send-api.service.ts @@ -135,11 +135,12 @@ export class SendApiService implements SendApiServiceAbstraction { return this.apiService.send("DELETE", "/sends/" + id, null, true, false); } - async save(sendData: [Send, EncArrayBuffer]): Promise { + async save(sendData: [Send, EncArrayBuffer]): Promise { const response = await this.upload(sendData); const data = new SendData(response); await this.sendService.upsert(data); + return new Send(data); } async delete(id: string): Promise { diff --git a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts index 1d93804e11f..07939ccb06c 100644 --- a/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts +++ b/libs/tools/send/send-ui/src/send-form/components/send-form.component.ts @@ -85,9 +85,14 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send submitBtn?: ButtonComponent; /** - * Event emitted when the send is saved successfully. + * Event emitted when the send is created successfully. */ - @Output() sendSaved = new EventEmitter(); + @Output() onSendCreated = new EventEmitter(); + + /** + * Event emitted when the send is updated successfully. + */ + @Output() onSendUpdated = new EventEmitter(); /** * The original send being edited or cloned. Null for add mode. @@ -200,22 +205,26 @@ export class SendFormComponent implements AfterViewInit, OnInit, OnChanges, Send return; } + const sendView = await this.addEditFormService.saveSend( + this.updatedSendView, + this.file, + this.config, + ); + + if (this.config.mode === "add") { + this.onSendCreated.emit(sendView); + return; + } + if (Utils.isNullOrWhitespace(this.updatedSendView.password)) { this.updatedSendView.password = null; } - await this.addEditFormService.saveSend(this.updatedSendView, this.file, this.config); - this.toastService.showToast({ variant: "success", title: null, - message: this.i18nService.t( - this.config.mode === "edit" || this.config.mode === "partial-edit" - ? "editedItem" - : "addedItem", - ), + message: this.i18nService.t("editedItem"), }); - - this.sendSaved.emit(this.updatedSendView); + this.onSendUpdated.emit(this.updatedSendView); }; } diff --git a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts index 9b6a6360ac7..9eb37b07e50 100644 --- a/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts +++ b/libs/tools/send/send-ui/src/send-form/services/default-send-form.service.ts @@ -19,6 +19,7 @@ export class DefaultSendFormService implements SendFormService { async saveSend(send: SendView, file: File | ArrayBuffer, config: SendFormConfig) { const sendData = await this.sendService.encrypt(send, file, send.password, null); - return await this.sendApiService.save(sendData); + const newSend = await this.sendApiService.save(sendData); + return await this.decryptSend(newSend); } } From 9ff1db757318a81c594d40849c4bbe01ab5d8193 Mon Sep 17 00:00:00 2001 From: Jared Snider <116684653+JaredSnider-Bitwarden@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:06:18 -0400 Subject: [PATCH 2/8] Auth/PM-9449 - UI Refresh + Client component consolidation into new LockV2 Component (#10451) * PM-9449 - Init stub of new lock comp * PM-9449 - (1) Add new lock screen title to all clients (2) Add to temp web routing module config * PM-9449 - LockV2Comp - Building now with web HTML * PM-9449 - Libs/Auth LockComp - bring in all desktop ts code; WIP, need to stand up LockCompService to facilitate ipc communication. * PM-9449 - Create LockComponentService for facilitating client logic; potentially will decompose later. * PM-9449 - Add extension lock comp service. * PM-9449 - Libs/auth LockComp - bring in browser extension logic * PM-9449 - Libs/auth LockComp html start * PM-9449 - Libs/Auth LockComp - (1) Remove unused dep (2) Update setEmailAsPageSubtitle to work. * PM-9449 - Add getBiometricsError to lock comp service for extension. * PM-9449 - LockComp - (1) Save off client type as public comp var (2) Rename biometricLock as biometricLockSet * PM-9449 - Work on lock comp service getAvailableUnlockOptions * PM-9449 - WIP libs/auth LockComp * PM-9449 - (1) Remove default lock comp svc (2) Add web lock comp svc. * PM-9449 - UnlockOptions - replace incorrect type * PM-9449 - DesktopLockComponentService -get most of observable based getAvailableUnlockOptions$ logic in place. * PM-9449 - LockCompSvc - getAvailableUnlockOptions in place for all clients. * PM-9449 - Add getBiometricsUnlockBtnText to LockCompSvc and put TODO for wiring it up later * PM-9449 - Lock Comp - Replace all manual bools with unlock options. * PM-9449 - Desktop Lock Comp Svc - adjust spacing * PM-9449 - LockCompSvc - remove biometricsEnabled method * PM-9449 - LockComp - Clean up commented out code * PM-9449 - LockComp - webVaultHostname --> envHostName * PM-9449 - Fix lock comp svc deps * PM-9449 - LockComp - HTML progress * PM-9449 - LockComp cleanup * PM-9449 - Web Routing Module - wire up lock vs lockv2 using extension swap * PM-9449 - Wire up loading state * PM-9449 - LockComp - start wiring up listenForActiveUnlockOptionChanges logic with reactivity * PM-9449 - Update desktop & extension lock comp service to use new biometrics service vs platform utils for biometrics information. * PM-9449 - LockV2 - Swap platform util usage with toast svc * PM-9449 - LockV2Comp - Bring over user id logic from PM-8933 * PM-9449 - LockV2Comp - Adjust everything to use activeAccount.id. * PM-9449 - LockV2Comp - Progress on wiring up unlock option reactive stream. * PM-9449 - LockComp ts - some refactoring and minor progress. * PM-9449 - LockComp HTML - refactoring based on new idea to keep unlock options as separate as possible. * PM-9449 - Add PIN translation to web * PM-9449 - (1) Lock HTML refactor to make as independent verticals as possible (2) Refactor Lock ts (3) LockSvc - replace type with enum. * PM-9449 - LockV2Comp - remove hardcoded await. * PM-9449 - LockComp HTML - add todo * PM-9449 - Web - Routing module - cleanup commented out stuff * PM-9449 - LockV2Comp - Wire up biometrics + mild refactor. * PM-9449 - Desktop - Wire up lockV2 redirection * PM-9449 - LockV2 - Desktop - don't focus until unlock opts defined. * PM-9449 - Fix accidental check in * PM-9449 - LockV2 - loading state depends on unlock opts * PM-9449 - LockV2 comp - remove unnecessary hr * PM-9449 - Migrate "yourVaultIsLockedV2" translation to desktop & browser. * PM-9449 - LockV2 - Layout tweaks for biometrics * PM-9449 - LockV2 - Biometric btn text * PM-9449 - LockV2 - Wire up biometrics loading / disable state + remove unnecessary conditions around biometricsUnlockBtnText * PM-9449 - DesktopLockSvc - Per discussion with Bernd, remove interval polling and just check once for biometric support and availability. * PM-9449 - AuthGuard - Add todo to remove promptBiometric * PM-9449 - LockV2 - Refactor primary and desktop init logic + misc clean up * PM-9449 - LockV2 - Reorder init methods * PM-9449 - LockV2 - Per discussion with Product, deprecate windows biometric settings update warning * PM-9449 - Add TODO per discussion with Justin and remove TODO * PM-9449 - LockV2 - Restore hide password on desktop window hidden functionality. * PM-9449 - Clean up accomplished todo * PM-9449 - LockV2 - Refactor func name. * PM-9449 - LockV2 Comp - (1) TODO cleanup (2) Add browser logic to handleBiometricsUnlockEnabled * PM-9449 - LockCompSvc changes - (1) Observability for isFido2Session (2) Adjust errors and returns per discussion with Justin * PM-9449 - Per product, no longer need to support special fido2 case on extension. * PM-9449 - LockCompSvc - add getPreviousUrl support * PM-9449 - LockV2 - Continued ts cleanup * PM-9449 - LockV2Comp - clean up unused props * PM-9449 - LockV2Comp - Rename response to masterPasswordVerificationResponse * PM-9449 - LockV2 - Remove unused formPromise prop * PM-9449 - Add missing translations + update desktop to showReadonlyHostName * PM-9449 - LockV2 - cleanup TODO * PM-9449 - LockV2 - more cleanup * PM-9449 - Desktop Routing Module - only allow LockV2 access if extension refresh flag is enabled. * PM-9449 - Extension - AppRoutingModule - Add extension redirect + new lockV2 route. * PM-9449 - Extension - AppRoutingModule - Add lockV2 to the ExtensionAnonLayoutWrapperComponent intead of the regular one. * PM-9449 - Extension - CurrentAccountComp - add null checks as anon layout components don't have a state today. This prevents the account switcher from working on the new lockV2 comp. * PM-9449 - Extension AppRoutingModule - LockV2 should use ExtensionAnonLayoutWrapperData * PM-9449 - LockComp - BiometricUnlock - cancelling is a valid action. * PM-9449 - LockV2 - Biometric autoprompt cleanup * PM-9449 - LockV2 - (1) Add TODO for KM team (2) Fix submit logic. * PM-9449 - Tweak TODO to add task # * PM-9449 - Test WebLockComponentService * PM-9449 - ExtensionLockComponentService tested * PM-9449 - Tweak extension lock comp svc test * PM-9449 - DesktopLockComponentService tested * PM-9449 - Add task # to TODO * PM-9449 - Update apps/browser/src/services/extension-lock-component.service.ts per PR feedback Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> * PM-9449 - Per PR feedback, replace from with defer for better reactive execution of promise based functions. * PM-9449 - Per PR feedback replace enum with type. * PM-9449 - Fix imports and tests due to key management file moves. * PM-9449 - Another test file import fix --------- Co-authored-by: Justin Baur <19896123+justindbaur@users.noreply.github.com> --- apps/browser/src/_locales/en/messages.json | 15 + .../current-account.component.ts | 2 +- apps/browser/src/popup/app-routing.module.ts | 25 + .../src/popup/services/services.module.ts | 8 +- .../extension-lock-component.service.spec.ts | 325 +++++++++ .../extension-lock-component.service.ts | 117 ++++ apps/desktop/src/app/app-routing.module.ts | 19 + .../src/app/services/services.module.ts | 8 +- apps/desktop/src/locales/en/messages.json | 18 + .../desktop-lock-component.service.spec.ts | 377 +++++++++++ .../desktop-lock-component.service.ts | 129 ++++ apps/web/src/app/auth/core/services/index.ts | 1 + .../web-lock-component.service.spec.ts | 94 +++ .../services/web-lock-component.service.ts | 55 ++ apps/web/src/app/core/core.module.ts | 12 +- apps/web/src/app/oss-routing.module.ts | 52 +- apps/web/src/locales/en/messages.json | 20 +- libs/angular/src/auth/guards/auth.guard.ts | 2 + libs/auth/src/angular/index.ts | 4 + .../angular/lock/lock-component.service.ts | 48 ++ .../auth/src/angular/lock/lock.component.html | 191 ++++++ libs/auth/src/angular/lock/lock.component.ts | 638 ++++++++++++++++++ 22 files changed, 2139 insertions(+), 21 deletions(-) create mode 100644 apps/browser/src/services/extension-lock-component.service.spec.ts create mode 100644 apps/browser/src/services/extension-lock-component.service.ts create mode 100644 apps/desktop/src/services/desktop-lock-component.service.spec.ts create mode 100644 apps/desktop/src/services/desktop-lock-component.service.ts create mode 100644 apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts create mode 100644 apps/web/src/app/auth/core/services/web-lock-component.service.ts create mode 100644 libs/auth/src/angular/lock/lock-component.service.ts create mode 100644 libs/auth/src/angular/lock/lock.component.html create mode 100644 libs/auth/src/angular/lock/lock.component.ts diff --git a/apps/browser/src/_locales/en/messages.json b/apps/browser/src/_locales/en/messages.json index 5203edf0a44..ec0fac137df 100644 --- a/apps/browser/src/_locales/en/messages.json +++ b/apps/browser/src/_locales/en/messages.json @@ -604,6 +604,15 @@ "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, + "yourVaultIsLockedV2": { + "message": "Your vault is locked" + }, + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "or": { + "message": "or" + }, "unlock": { "message": "Unlock" }, @@ -1936,6 +1945,9 @@ "unlockWithBiometrics": { "message": "Unlock with biometrics" }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "awaitDesktop": { "message": "Awaiting confirmation from desktop" }, @@ -3623,6 +3635,9 @@ "typePasskey": { "message": "Passkey" }, + "accessing": { + "message": "Accessing" + }, "passkeyNotCopied": { "message": "Passkey will not be copied" }, diff --git a/apps/browser/src/auth/popup/account-switching/current-account.component.ts b/apps/browser/src/auth/popup/account-switching/current-account.component.ts index 6c7c1e7d92f..12210b2b452 100644 --- a/apps/browser/src/auth/popup/account-switching/current-account.component.ts +++ b/apps/browser/src/auth/popup/account-switching/current-account.component.ts @@ -59,7 +59,7 @@ export class CurrentAccountComponent { } async currentAccountClicked() { - if (this.route.snapshot.data.state.includes("account-switcher")) { + if (this.route.snapshot.data?.state?.includes("account-switcher")) { this.location.back(); } else { await this.router.navigate(["/account-switcher"]); diff --git a/apps/browser/src/popup/app-routing.module.ts b/apps/browser/src/popup/app-routing.module.ts index d540ea39edc..9fd52470c0a 100644 --- a/apps/browser/src/popup/app-routing.module.ts +++ b/apps/browser/src/popup/app-routing.module.ts @@ -17,6 +17,8 @@ import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + LockIcon, + LockV2Component, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -181,6 +183,7 @@ const routes: Routes = [ path: "lock", component: LockComponent, canActivate: [lockGuard()], + canMatch: [extensionRefreshRedirect("/lockV2")], data: { state: "lock", doNotSaveUrl: true } satisfies RouteDataProperties, }, ...twofactorRefactorSwap( @@ -438,6 +441,28 @@ const routes: Routes = [ ], }, ), + { + path: "", + component: ExtensionAnonLayoutWrapperComponent, + children: [ + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: "yourVaultIsLockedV2", + showReadonlyHostname: true, + showAcctSwitcher: true, + } satisfies ExtensionAnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, + ], + }, { path: "", component: AnonLayoutWrapperComponent, diff --git a/apps/browser/src/popup/services/services.module.ts b/apps/browser/src/popup/services/services.module.ts index 483bf86712a..024b4f46315 100644 --- a/apps/browser/src/popup/services/services.module.ts +++ b/apps/browser/src/popup/services/services.module.ts @@ -16,7 +16,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { AnonLayoutWrapperDataService } from "@bitwarden/auth/angular"; +import { AnonLayoutWrapperDataService, LockComponentService } from "@bitwarden/auth/angular"; import { LockService, PinServiceAbstraction } from "@bitwarden/auth/common"; import { EventCollectionService as EventCollectionServiceAbstraction } from "@bitwarden/common/abstractions/event/event-collection.service"; import { NotificationsService } from "@bitwarden/common/abstractions/notifications.service"; @@ -117,6 +117,7 @@ import { ForegroundTaskSchedulerService } from "../../platform/services/task-sch import { BrowserStorageServiceProvider } from "../../platform/storage/browser-storage-service.provider"; import { ForegroundMemoryStorageService } from "../../platform/storage/foreground-memory-storage.service"; import { fromChromeRuntimeMessaging } from "../../platform/utils/from-chrome-runtime-messaging"; +import { ExtensionLockComponentService } from "../../services/extension-lock-component.service"; import { ForegroundVaultTimeoutService } from "../../services/vault-timeout/foreground-vault-timeout.service"; import { BrowserSendStateService } from "../../tools/popup/services/browser-send-state.service"; import { FilePopoutUtilsService } from "../../tools/popup/services/file-popout-utils.service"; @@ -536,6 +537,11 @@ const safeProviders: SafeProvider[] = [ provide: CLIENT_TYPE, useValue: ClientType.Browser, }), + safeProvider({ + provide: LockComponentService, + useClass: ExtensionLockComponentService, + deps: [], + }), safeProvider({ provide: Fido2UserVerificationService, useClass: Fido2UserVerificationService, diff --git a/apps/browser/src/services/extension-lock-component.service.spec.ts b/apps/browser/src/services/extension-lock-component.service.spec.ts new file mode 100644 index 00000000000..f537897cf8d --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.spec.ts @@ -0,0 +1,325 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +import { ExtensionLockComponentService } from "./extension-lock-component.service"; + +describe("ExtensionLockComponentService", () => { + let service: ExtensionLockComponentService; + + let userDecryptionOptionsService: MockProxy; + let platformUtilsService: MockProxy; + let biometricsService: MockProxy; + let pinService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; + let cryptoService: MockProxy; + let routerService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + platformUtilsService = mock(); + biometricsService = mock(); + pinService = mock(); + vaultTimeoutSettingsService = mock(); + cryptoService = mock(); + routerService = mock(); + + TestBed.configureTestingModule({ + providers: [ + ExtensionLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + { + provide: PlatformUtilsService, + useValue: platformUtilsService, + }, + { + provide: BiometricsService, + useValue: biometricsService, + }, + { + provide: PinServiceAbstraction, + useValue: pinService, + }, + { + provide: VaultTimeoutSettingsService, + useValue: vaultTimeoutSettingsService, + }, + { + provide: CryptoService, + useValue: cryptoService, + }, + { + provide: BrowserRouterService, + useValue: routerService, + }, + ], + }); + + service = TestBed.inject(ExtensionLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getPreviousUrl", () => { + it("returns the previous URL", () => { + routerService.getPreviousUrl.mockReturnValue("previousUrl"); + expect(service.getPreviousUrl()).toBe("previousUrl"); + }); + }); + + describe("getBiometricsError", () => { + it("returns a biometric error description when given a valid error type", () => { + expect( + service.getBiometricsError({ + message: "startDesktop", + }), + ).toBe("startDesktopDesc"); + }); + + it("returns null when given an invalid error type", () => { + expect( + service.getBiometricsError({ + message: "invalidError", + }), + ).toBeNull(); + }); + + it("returns null when given a null input", () => { + expect(service.getBiometricsError(null)).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("throws an error", async () => { + await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("returns the biometric unlock button text", () => { + expect(service.getBiometricsUnlockBtnText()).toBe("unlockWithBiometrics"); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + interface MockInputs { + hasMasterPassword: boolean; + osSupportsBiometric: boolean; + biometricLockSet: boolean; + hasBiometricEncryptedUserKeyStored: boolean; + platformSupportsSecureStorage: boolean; + pinDecryptionAvailable: boolean; + } + + const table: [MockInputs, UnlockOptions][] = [ + [ + // MP + PIN + Biometrics available + { + hasMasterPassword: true, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: true, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // PIN + Biometrics available + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: no user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics not available: biometric lock not set + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: false, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: user key not stored + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: OS doesn't support + { + hasMasterPassword: false, + osSupportsBiometric: false, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + }, + }, + ], + ]; + + test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => { + const userId = "userId" as UserId; + const userDecryptionOptions = { + hasMasterPassword: mockInputs.hasMasterPassword, + }; + + // MP + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + + // Biometrics + biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); + platformUtilsService.supportsSecureStorage.mockReturnValue( + mockInputs.platformSupportsSecureStorage, + ); + + // PIN + pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual(expectedOutput); + }); + }); +}); diff --git a/apps/browser/src/services/extension-lock-component.service.ts b/apps/browser/src/services/extension-lock-component.service.ts new file mode 100644 index 00000000000..58514fa2b17 --- /dev/null +++ b/apps/browser/src/services/extension-lock-component.service.ts @@ -0,0 +1,117 @@ +import { inject } from "@angular/core"; +import { combineLatest, defer, map, Observable } from "rxjs"; + +import { + BiometricsDisableReason, + LockComponentService, + UnlockOptions, +} from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { BiometricErrors, BiometricErrorTypes } from "../models/biometricErrors"; +import { BrowserRouterService } from "../platform/popup/services/browser-router.service"; + +export class ExtensionLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private readonly cryptoService = inject(CryptoService); + private readonly routerService = inject(BrowserRouterService); + + getPreviousUrl(): string | null { + return this.routerService.getPreviousUrl(); + } + + getBiometricsError(error: any): string | null { + const biometricsError = BiometricErrors[error?.message as BiometricErrorTypes]; + + if (!biometricsError) { + return null; + } + + return biometricsError.description; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + return "unlockWithBiometrics"; + } + + private async isBiometricLockSet(userId: UserId): Promise { + const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); + const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored( + KeySuffixOptions.Biometric, + userId, + ); + const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); + + return ( + biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) + ); + } + + private getBiometricsDisabledReason( + osSupportsBiometric: boolean, + biometricLockSet: boolean, + ): BiometricsDisableReason | null { + if (!osSupportsBiometric) { + return BiometricsDisableReason.NotSupportedOnOperatingSystem; + } else if (!biometricLockSet) { + return BiometricsDisableReason.EncryptedKeysUnavailable; + } + + return null; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(() => this.biometricsService.supportsBiometric()), + defer(() => this.isBiometricLockSet(userId)), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map( + ([ + supportsBiometric, + isBiometricsLockSet, + userDecryptionOptions, + pinDecryptionAvailable, + ]) => { + const disableReason = this.getBiometricsDisabledReason( + supportsBiometric, + isBiometricsLockSet, + ); + + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: supportsBiometric && isBiometricsLockSet, + disableReason: disableReason, + }, + }; + return unlockOpts; + }, + ), + ); + } +} diff --git a/apps/desktop/src/app/app-routing.module.ts b/apps/desktop/src/app/app-routing.module.ts index 1e13be12a73..86a39163f3a 100644 --- a/apps/desktop/src/app/app-routing.module.ts +++ b/apps/desktop/src/app/app-routing.module.ts @@ -11,9 +11,12 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { extensionRefreshRedirect } from "@bitwarden/angular/utils/extension-refresh-redirect"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, + LockIcon, + LockV2Component, PasswordHintComponent, RegistrationFinishComponent, RegistrationStartComponent, @@ -62,6 +65,7 @@ const routes: Routes = [ path: "lock", component: LockComponent, canActivate: [lockGuard()], + canMatch: [extensionRefreshRedirect("/lockV2")], }, { path: "login", @@ -190,6 +194,21 @@ const routes: Routes = [ }, ], }, + { + path: "lockV2", + canActivate: [canAccessFeature(FeatureFlag.ExtensionRefresh), lockGuard()], + data: { + pageIcon: LockIcon, + pageTitle: "yourVaultIsLockedV2", + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + children: [ + { + path: "", + component: LockV2Component, + }, + ], + }, { path: "set-password-jit", canActivate: [canAccessFeature(FeatureFlag.EmailVerification)], diff --git a/apps/desktop/src/app/services/services.module.ts b/apps/desktop/src/app/services/services.module.ts index d3d41d277b6..c6b73fbbbca 100644 --- a/apps/desktop/src/app/services/services.module.ts +++ b/apps/desktop/src/app/services/services.module.ts @@ -19,7 +19,7 @@ import { CLIENT_TYPE, } from "@bitwarden/angular/services/injection-tokens"; import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services.module"; -import { SetPasswordJitService } from "@bitwarden/auth/angular"; +import { LockComponentService, SetPasswordJitService } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction, PinServiceAbstraction, @@ -86,6 +86,7 @@ import { ElectronRendererStorageService } from "../../platform/services/electron import { I18nRendererService } from "../../platform/services/i18n.renderer.service"; import { fromIpcMessaging } from "../../platform/utils/from-ipc-messaging"; import { fromIpcSystemTheme } from "../../platform/utils/from-ipc-system-theme"; +import { DesktopLockComponentService } from "../../services/desktop-lock-component.service"; import { EncryptedMessageHandlerService } from "../../services/encrypted-message-handler.service"; import { NativeMessageHandlerService } from "../../services/native-message-handler.service"; import { NativeMessagingService } from "../../services/native-messaging.service"; @@ -277,6 +278,11 @@ const safeProviders: SafeProvider[] = [ useClass: NativeMessagingManifestService, deps: [], }), + safeProvider({ + provide: LockComponentService, + useClass: DesktopLockComponentService, + deps: [], + }), safeProvider({ provide: CLIENT_TYPE, useValue: ClientType.Desktop, diff --git a/apps/desktop/src/locales/en/messages.json b/apps/desktop/src/locales/en/messages.json index 9504ecb1fa3..0b7a9c678c9 100644 --- a/apps/desktop/src/locales/en/messages.json +++ b/apps/desktop/src/locales/en/messages.json @@ -918,6 +918,18 @@ "yourVaultIsLocked": { "message": "Your vault is locked. Verify your identity to continue." }, + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "or": { + "message": "or" + }, + "unlockWithBiometrics": { + "message": "Unlock with biometrics" + }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "unlock": { "message": "Unlock" }, @@ -2256,6 +2268,9 @@ "locked": { "message": "Locked" }, + "yourVaultIsLockedV2": { + "message": "Your vault is locked" + }, "unlocked": { "message": "Unlocked" }, @@ -2608,6 +2623,9 @@ "important": { "message": "Important:" }, + "accessing": { + "message": "Accessing" + }, "accessTokenUnableToBeDecrypted": { "message": "You have been logged out because your access token could not be decrypted. Please log in again to resolve this issue." }, diff --git a/apps/desktop/src/services/desktop-lock-component.service.spec.ts b/apps/desktop/src/services/desktop-lock-component.service.spec.ts new file mode 100644 index 00000000000..ff1f8328ea3 --- /dev/null +++ b/apps/desktop/src/services/desktop-lock-component.service.spec.ts @@ -0,0 +1,377 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { BiometricsDisableReason, UnlockOptions } from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +import { DesktopLockComponentService } from "./desktop-lock-component.service"; + +// ipc mock global +const isWindowVisibleMock = jest.fn(); +const biometricEnabledMock = jest.fn(); +(global as any).ipc = { + keyManagement: { + biometric: { + enabled: biometricEnabledMock, + }, + }, + platform: { + isWindowVisible: isWindowVisibleMock, + }, +}; + +describe("DesktopLockComponentService", () => { + let service: DesktopLockComponentService; + + let userDecryptionOptionsService: MockProxy; + let platformUtilsService: MockProxy; + let biometricsService: MockProxy; + let pinService: MockProxy; + let vaultTimeoutSettingsService: MockProxy; + let cryptoService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + platformUtilsService = mock(); + biometricsService = mock(); + pinService = mock(); + vaultTimeoutSettingsService = mock(); + cryptoService = mock(); + + TestBed.configureTestingModule({ + providers: [ + DesktopLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + { + provide: PlatformUtilsService, + useValue: platformUtilsService, + }, + { + provide: BiometricsService, + useValue: biometricsService, + }, + { + provide: PinServiceAbstraction, + useValue: pinService, + }, + { + provide: VaultTimeoutSettingsService, + useValue: vaultTimeoutSettingsService, + }, + { + provide: CryptoService, + useValue: cryptoService, + }, + ], + }); + + service = TestBed.inject(DesktopLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + // getBiometricsError + describe("getBiometricsError", () => { + it("returns null when given null", () => { + const result = service.getBiometricsError(null); + expect(result).toBeNull(); + }); + + it("returns null when given an unknown error", () => { + const result = service.getBiometricsError({ message: "unknown" }); + expect(result).toBeNull(); + }); + }); + + describe("getPreviousUrl", () => { + it("returns null", () => { + const result = service.getPreviousUrl(); + expect(result).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("returns the window visibility", async () => { + isWindowVisibleMock.mockReturnValue(true); + const result = await service.isWindowVisible(); + expect(result).toBe(true); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("returns the correct text for Mac OS", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.MacOsDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithTouchId"); + }); + + it("returns the correct text for Windows", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.WindowsDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithWindowsHello"); + }); + + it("returns the correct text for Linux", () => { + platformUtilsService.getDevice.mockReturnValue(DeviceType.LinuxDesktop); + const result = service.getBiometricsUnlockBtnText(); + expect(result).toBe("unlockWithPolkit"); + }); + + it("throws an error for an unsupported platform", () => { + platformUtilsService.getDevice.mockReturnValue("unsupported" as any); + expect(() => service.getBiometricsUnlockBtnText()).toThrowError("Unsupported platform"); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + interface MockInputs { + hasMasterPassword: boolean; + osSupportsBiometric: boolean; + biometricLockSet: boolean; + biometricReady: boolean; + hasBiometricEncryptedUserKeyStored: boolean; + platformSupportsSecureStorage: boolean; + pinDecryptionAvailable: boolean; + } + + const table: [MockInputs, UnlockOptions][] = [ + [ + // MP + PIN + Biometrics available + { + hasMasterPassword: true, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: true, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // PIN + Biometrics available + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: true, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: true, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics available: no user key stored with no secure storage + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + biometricReady: true, + platformSupportsSecureStorage: false, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: true, + disableReason: null, + }, + }, + ], + [ + // Biometrics not available: biometric not ready + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: false, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.SystemBiometricsUnavailable, + }, + }, + ], + [ + // Biometrics not available: biometric lock not set + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: false, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: user key not stored + { + hasMasterPassword: false, + osSupportsBiometric: true, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: false, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.EncryptedKeysUnavailable, + }, + }, + ], + [ + // Biometrics not available: OS doesn't support + { + hasMasterPassword: false, + osSupportsBiometric: false, + biometricLockSet: true, + hasBiometricEncryptedUserKeyStored: true, + biometricReady: true, + platformSupportsSecureStorage: true, + pinDecryptionAvailable: false, + }, + { + masterPassword: { + enabled: false, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: BiometricsDisableReason.NotSupportedOnOperatingSystem, + }, + }, + ], + ]; + + test.each(table)("returns unlock options", async (mockInputs, expectedOutput) => { + const userId = "userId" as UserId; + const userDecryptionOptions = { + hasMasterPassword: mockInputs.hasMasterPassword, + }; + + // MP + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValue( + of(userDecryptionOptions), + ); + + // Biometrics + biometricsService.supportsBiometric.mockResolvedValue(mockInputs.osSupportsBiometric); + vaultTimeoutSettingsService.isBiometricLockSet.mockResolvedValue(mockInputs.biometricLockSet); + cryptoService.hasUserKeyStored.mockResolvedValue( + mockInputs.hasBiometricEncryptedUserKeyStored, + ); + platformUtilsService.supportsSecureStorage.mockReturnValue( + mockInputs.platformSupportsSecureStorage, + ); + biometricEnabledMock.mockResolvedValue(mockInputs.biometricReady); + + // PIN + pinService.isPinDecryptionAvailable.mockResolvedValue(mockInputs.pinDecryptionAvailable); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual(expectedOutput); + }); + }); +}); diff --git a/apps/desktop/src/services/desktop-lock-component.service.ts b/apps/desktop/src/services/desktop-lock-component.service.ts new file mode 100644 index 00000000000..f31ee93a726 --- /dev/null +++ b/apps/desktop/src/services/desktop-lock-component.service.ts @@ -0,0 +1,129 @@ +import { inject } from "@angular/core"; +import { combineLatest, defer, map, Observable } from "rxjs"; + +import { + BiometricsDisableReason, + LockComponentService, + UnlockOptions, +} from "@bitwarden/auth/angular"; +import { + PinServiceAbstraction, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { VaultTimeoutSettingsService } from "@bitwarden/common/abstractions/vault-timeout/vault-timeout-settings.service"; +import { DeviceType } from "@bitwarden/common/enums"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { UserId } from "@bitwarden/common/types/guid"; +import { BiometricsService } from "@bitwarden/key-management"; + +export class DesktopLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + private readonly platformUtilsService = inject(PlatformUtilsService); + private readonly biometricsService = inject(BiometricsService); + private readonly pinService = inject(PinServiceAbstraction); + private readonly vaultTimeoutSettingsService = inject(VaultTimeoutSettingsService); + private readonly cryptoService = inject(CryptoService); + + constructor() {} + + getBiometricsError(error: any): string | null { + return null; + } + + getPreviousUrl(): string | null { + return null; + } + + async isWindowVisible(): Promise { + return ipc.platform.isWindowVisible(); + } + + getBiometricsUnlockBtnText(): string { + switch (this.platformUtilsService.getDevice()) { + case DeviceType.MacOsDesktop: + return "unlockWithTouchId"; + case DeviceType.WindowsDesktop: + return "unlockWithWindowsHello"; + case DeviceType.LinuxDesktop: + return "unlockWithPolkit"; + default: + throw new Error("Unsupported platform"); + } + } + + private async isBiometricLockSet(userId: UserId): Promise { + const biometricLockSet = await this.vaultTimeoutSettingsService.isBiometricLockSet(userId); + const hasBiometricEncryptedUserKeyStored = await this.cryptoService.hasUserKeyStored( + KeySuffixOptions.Biometric, + userId, + ); + const platformSupportsSecureStorage = this.platformUtilsService.supportsSecureStorage(); + + return ( + biometricLockSet && (hasBiometricEncryptedUserKeyStored || !platformSupportsSecureStorage) + ); + } + + private async isBiometricsSupportedAndReady( + userId: UserId, + ): Promise<{ supportsBiometric: boolean; biometricReady: boolean }> { + const supportsBiometric = await this.biometricsService.supportsBiometric(); + const biometricReady = await ipc.keyManagement.biometric.enabled(userId); + return { supportsBiometric, biometricReady }; + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return combineLatest([ + // Note: defer is preferable b/c it delays the execution of the function until the observable is subscribed to + defer(() => this.isBiometricsSupportedAndReady(userId)), + defer(() => this.isBiometricLockSet(userId)), + this.userDecryptionOptionsService.userDecryptionOptionsById$(userId), + defer(() => this.pinService.isPinDecryptionAvailable(userId)), + ]).pipe( + map( + ([biometricsData, isBiometricsLockSet, userDecryptionOptions, pinDecryptionAvailable]) => { + const disableReason = this.getBiometricsDisabledReason( + biometricsData.supportsBiometric, + isBiometricsLockSet, + biometricsData.biometricReady, + ); + + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: pinDecryptionAvailable, + }, + biometrics: { + enabled: + biometricsData.supportsBiometric && + isBiometricsLockSet && + biometricsData.biometricReady, + disableReason: disableReason, + }, + }; + + return unlockOpts; + }, + ), + ); + } + + private getBiometricsDisabledReason( + osSupportsBiometric: boolean, + biometricLockSet: boolean, + biometricReady: boolean, + ): BiometricsDisableReason | null { + if (!osSupportsBiometric) { + return BiometricsDisableReason.NotSupportedOnOperatingSystem; + } else if (!biometricLockSet) { + return BiometricsDisableReason.EncryptedKeysUnavailable; + } else if (!biometricReady) { + return BiometricsDisableReason.SystemBiometricsUnavailable; + } + return null; + } +} diff --git a/apps/web/src/app/auth/core/services/index.ts b/apps/web/src/app/auth/core/services/index.ts index c85f0f3204c..9e433b87f36 100644 --- a/apps/web/src/app/auth/core/services/index.ts +++ b/apps/web/src/app/auth/core/services/index.ts @@ -1,3 +1,4 @@ export * from "./webauthn-login"; export * from "./set-password-jit"; export * from "./registration"; +export * from "./web-lock-component.service"; diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts b/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts new file mode 100644 index 00000000000..5eb26a8c76c --- /dev/null +++ b/apps/web/src/app/auth/core/services/web-lock-component.service.spec.ts @@ -0,0 +1,94 @@ +import { TestBed } from "@angular/core/testing"; +import { mock, MockProxy } from "jest-mock-extended"; +import { firstValueFrom, of } from "rxjs"; + +import { UserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; + +import { WebLockComponentService } from "./web-lock-component.service"; + +describe("WebLockComponentService", () => { + let service: WebLockComponentService; + + let userDecryptionOptionsService: MockProxy; + + beforeEach(() => { + userDecryptionOptionsService = mock(); + + TestBed.configureTestingModule({ + providers: [ + WebLockComponentService, + { + provide: UserDecryptionOptionsServiceAbstraction, + useValue: userDecryptionOptionsService, + }, + ], + }); + + service = TestBed.inject(WebLockComponentService); + }); + + it("instantiates", () => { + expect(service).not.toBeFalsy(); + }); + + describe("getBiometricsError", () => { + it("throws an error when given a null input", () => { + expect(() => service.getBiometricsError(null)).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + it("throws an error when given a non-null input", () => { + expect(() => service.getBiometricsError("error")).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + }); + + describe("getPreviousUrl", () => { + it("returns null", () => { + expect(service.getPreviousUrl()).toBeNull(); + }); + }); + + describe("isWindowVisible", () => { + it("throws an error", async () => { + await expect(service.isWindowVisible()).rejects.toThrow("Method not implemented."); + }); + }); + + describe("getBiometricsUnlockBtnText", () => { + it("throws an error", () => { + expect(() => service.getBiometricsUnlockBtnText()).toThrow( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + }); + }); + + describe("getAvailableUnlockOptions$", () => { + it("returns an observable of unlock options", async () => { + const userId = "user-id" as UserId; + const userDecryptionOptions = { + hasMasterPassword: true, + }; + userDecryptionOptionsService.userDecryptionOptionsById$.mockReturnValueOnce( + of(userDecryptionOptions), + ); + + const unlockOptions = await firstValueFrom(service.getAvailableUnlockOptions$(userId)); + + expect(unlockOptions).toEqual({ + masterPassword: { + enabled: true, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: null, + }, + }); + }); + }); +}); diff --git a/apps/web/src/app/auth/core/services/web-lock-component.service.ts b/apps/web/src/app/auth/core/services/web-lock-component.service.ts new file mode 100644 index 00000000000..e24f299e23b --- /dev/null +++ b/apps/web/src/app/auth/core/services/web-lock-component.service.ts @@ -0,0 +1,55 @@ +import { inject } from "@angular/core"; +import { map, Observable } from "rxjs"; + +import { LockComponentService, UnlockOptions } from "@bitwarden/auth/angular"; +import { + UserDecryptionOptions, + UserDecryptionOptionsServiceAbstraction, +} from "@bitwarden/auth/common"; +import { UserId } from "@bitwarden/common/types/guid"; + +export class WebLockComponentService implements LockComponentService { + private readonly userDecryptionOptionsService = inject(UserDecryptionOptionsServiceAbstraction); + + constructor() {} + + getBiometricsError(error: any): string | null { + throw new Error( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + } + + getPreviousUrl(): string | null { + return null; + } + + async isWindowVisible(): Promise { + throw new Error("Method not implemented."); + } + + getBiometricsUnlockBtnText(): string { + throw new Error( + "Biometric unlock is not supported in the web app. See getAvailableUnlockOptions$", + ); + } + + getAvailableUnlockOptions$(userId: UserId): Observable { + return this.userDecryptionOptionsService.userDecryptionOptionsById$(userId).pipe( + map((userDecryptionOptions: UserDecryptionOptions) => { + const unlockOpts: UnlockOptions = { + masterPassword: { + enabled: userDecryptionOptions.hasMasterPassword, + }, + pin: { + enabled: false, + }, + biometrics: { + enabled: false, + disableReason: null, + }, + }; + return unlockOpts; + }), + ); + } +} diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index 419794fe3bc..c14c9750474 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -20,6 +20,7 @@ import { JslibServicesModule } from "@bitwarden/angular/services/jslib-services. import { ModalService as ModalServiceAbstraction } from "@bitwarden/angular/services/modal.service"; import { RegistrationFinishService as RegistrationFinishServiceAbstraction, + LockComponentService, SetPasswordJitService, } from "@bitwarden/auth/angular"; import { InternalUserDecryptionOptionsServiceAbstraction } from "@bitwarden/auth/common"; @@ -62,7 +63,11 @@ import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/va import { BiometricsService } from "@bitwarden/key-management"; import { PolicyListService } from "../admin-console/core/policy-list.service"; -import { WebRegistrationFinishService, WebSetPasswordJitService } from "../auth"; +import { + WebSetPasswordJitService, + WebRegistrationFinishService, + WebLockComponentService, +} from "../auth"; import { AcceptOrganizationInviteService } from "../auth/organization-invite/accept-organization.service"; import { HtmlStorageService } from "../core/html-storage.service"; import { I18nService } from "../core/i18n.service"; @@ -197,6 +202,11 @@ const safeProviders: SafeProvider[] = [ PolicyService, ], }), + safeProvider({ + provide: LockComponentService, + useClass: WebLockComponentService, + deps: [], + }), safeProvider({ provide: SetPasswordJitService, useClass: WebSetPasswordJitService, diff --git a/apps/web/src/app/oss-routing.module.ts b/apps/web/src/app/oss-routing.module.ts index cae73e81595..983067823cb 100644 --- a/apps/web/src/app/oss-routing.module.ts +++ b/apps/web/src/app/oss-routing.module.ts @@ -10,6 +10,7 @@ import { unauthGuardFn, } from "@bitwarden/angular/auth/guards"; import { canAccessFeature } from "@bitwarden/angular/platform/guard/feature-flag.guard"; +import { extensionRefreshSwap } from "@bitwarden/angular/utils/extension-refresh-swap"; import { AnonLayoutWrapperComponent, AnonLayoutWrapperData, @@ -20,6 +21,7 @@ import { RegistrationStartSecondaryComponentData, SetPasswordJitComponent, RegistrationLinkExpiredComponent, + LockV2Component, LockIcon, UserLockIcon, } from "@bitwarden/auth/angular"; @@ -337,21 +339,41 @@ const routes: Routes = [ pageTitle: "logIn", }, }, - { - path: "lock", - canActivate: [deepLinkGuard(), lockGuard()], - children: [ - { - path: "", - component: LockComponent, - }, - ], - data: { - pageTitle: "yourVaultIsLockedV2", - pageIcon: LockIcon, - showReadonlyHostname: true, - } satisfies AnonLayoutWrapperData, - }, + ...extensionRefreshSwap( + LockComponent, + LockV2Component, + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockComponent, + }, + ], + data: { + pageTitle: "yourVaultIsLockedV2", + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, + { + path: "lock", + canActivate: [deepLinkGuard(), lockGuard()], + children: [ + { + path: "", + component: LockV2Component, + }, + ], + data: { + pageTitle: "yourAccountIsLocked", + pageIcon: LockIcon, + showReadonlyHostname: true, + } satisfies AnonLayoutWrapperData, + }, + ), + { path: "2fa", canActivate: [unauthGuardFn()], diff --git a/apps/web/src/locales/en/messages.json b/apps/web/src/locales/en/messages.json index 8e847dfb63e..ab43c3af18b 100644 --- a/apps/web/src/locales/en/messages.json +++ b/apps/web/src/locales/en/messages.json @@ -1099,8 +1099,11 @@ "yourVaultIsLockedV2": { "message": "Your vault is locked" }, - "uuid": { - "message": "UUID" + "yourAccountIsLocked": { + "message": "Your account is locked" + }, + "uuid":{ + "message" : "UUID" }, "unlock": { "message": "Unlock" @@ -3169,6 +3172,10 @@ "incorrectPin": { "message": "Incorrect PIN" }, + "pin": { + "message": "PIN", + "description": "PIN code. Ex. The short code (often numeric) that you use to unlock a device." + }, "exportedVault": { "message": "Vault exported" }, @@ -7463,6 +7470,15 @@ "or": { "message": "or" }, + "unlockWithBiometrics": { + "message": "Unlock with biometrics" + }, + "unlockWithPin": { + "message": "Unlock with PIN" + }, + "unlockWithMasterPassword": { + "message": "Unlock with master password" + }, "licenseAndBillingManagement": { "message": "License and billing management" }, diff --git a/libs/angular/src/auth/guards/auth.guard.ts b/libs/angular/src/auth/guards/auth.guard.ts index b54f114d3d4..1486b9b57d8 100644 --- a/libs/angular/src/auth/guards/auth.guard.ts +++ b/libs/angular/src/auth/guards/auth.guard.ts @@ -38,6 +38,8 @@ export const authGuard: CanActivateFn = async ( if (routerState != null) { messagingService.send("lockedUrl", { url: routerState.url }); } + // TODO PM-9674: when extension refresh is finished, remove promptBiometric + // as it has been integrated into the component as a default feature. return router.createUrlTree(["lock"], { queryParams: { promptBiometric: true } }); } diff --git a/libs/auth/src/angular/index.ts b/libs/auth/src/angular/index.ts index bfb3a67aedc..6de473c33e7 100644 --- a/libs/auth/src/angular/index.ts +++ b/libs/auth/src/angular/index.ts @@ -43,5 +43,9 @@ export * from "./registration/registration-env-selector/registration-env-selecto export * from "./registration/registration-finish/registration-finish.service"; export * from "./registration/registration-finish/default-registration-finish.service"; +// lock +export * from "./lock/lock.component"; +export * from "./lock/lock-component.service"; + // vault timeout export * from "./vault-timeout-input/vault-timeout-input.component"; diff --git a/libs/auth/src/angular/lock/lock-component.service.ts b/libs/auth/src/angular/lock/lock-component.service.ts new file mode 100644 index 00000000000..fe54db21baa --- /dev/null +++ b/libs/auth/src/angular/lock/lock-component.service.ts @@ -0,0 +1,48 @@ +import { Observable } from "rxjs"; + +import { UserId } from "@bitwarden/common/types/guid"; + +export enum BiometricsDisableReason { + NotSupportedOnOperatingSystem = "NotSupportedOnOperatingSystem", + EncryptedKeysUnavailable = "BiometricsEncryptedKeysUnavailable", + SystemBiometricsUnavailable = "SystemBiometricsUnavailable", +} + +// ex: type UnlockOptionValue = "masterPassword" | "pin" | "biometrics" +export type UnlockOptionValue = (typeof UnlockOption)[keyof typeof UnlockOption]; + +export const UnlockOption = Object.freeze({ + MasterPassword: "masterPassword", + Pin: "pin", + Biometrics: "biometrics", +}) satisfies { [Prop in keyof UnlockOptions as Capitalize]: Prop }; + +export type UnlockOptions = { + masterPassword: { + enabled: boolean; + }; + pin: { + enabled: boolean; + }; + biometrics: { + enabled: boolean; + disableReason: BiometricsDisableReason | null; + }; +}; + +/** + * The LockComponentService is a service which allows the single libs/auth LockComponent to delegate all + * client specific functionality to client specific services implementations of LockComponentService. + */ +export abstract class LockComponentService { + // Extension + abstract getBiometricsError(error: any): string | null; + abstract getPreviousUrl(): string | null; + + // Desktop only + abstract isWindowVisible(): Promise; + abstract getBiometricsUnlockBtnText(): string; + + // Multi client + abstract getAvailableUnlockOptions$(userId: UserId): Observable; +} diff --git a/libs/auth/src/angular/lock/lock.component.html b/libs/auth/src/angular/lock/lock.component.html new file mode 100644 index 00000000000..5f5991c681e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.html @@ -0,0 +1,191 @@ + +
+ +
+
+ + + + + + +
+

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+ + + +
+ + {{ "pin" | i18n }} + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+ + + +
+ + {{ "masterPass" | i18n }} + + + + + + +
+ + +

{{ "or" | i18n }}

+ + + + + + + + + + +
+
+
+
diff --git a/libs/auth/src/angular/lock/lock.component.ts b/libs/auth/src/angular/lock/lock.component.ts new file mode 100644 index 00000000000..7bea14f221e --- /dev/null +++ b/libs/auth/src/angular/lock/lock.component.ts @@ -0,0 +1,638 @@ +import { CommonModule } from "@angular/common"; +import { Component, NgZone, OnDestroy, OnInit } from "@angular/core"; +import { FormBuilder, FormGroup, ReactiveFormsModule, Validators } from "@angular/forms"; +import { Router } from "@angular/router"; +import { BehaviorSubject, firstValueFrom, Subject, switchMap, take, takeUntil } from "rxjs"; + +import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { InternalPolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; +import { MasterPasswordPolicyOptions } from "@bitwarden/common/admin-console/models/domain/master-password-policy-options"; +import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { DeviceTrustServiceAbstraction } from "@bitwarden/common/auth/abstractions/device-trust.service.abstraction"; +import { InternalMasterPasswordServiceAbstraction } from "@bitwarden/common/auth/abstractions/master-password.service.abstraction"; +import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; +import { VerificationType } from "@bitwarden/common/auth/enums/verification-type"; +import { ForceSetPasswordReason } from "@bitwarden/common/auth/models/domain/force-set-password-reason"; +import { + MasterPasswordVerification, + MasterPasswordVerificationResponse, +} from "@bitwarden/common/auth/types/verification"; +import { ClientType } from "@bitwarden/common/enums"; +import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; +import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; +import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; +import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { KeySuffixOptions } from "@bitwarden/common/platform/enums"; +import { SyncService } from "@bitwarden/common/platform/sync"; +import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; +import { UserId } from "@bitwarden/common/types/guid"; +import { UserKey } from "@bitwarden/common/types/key"; +import { + AsyncActionsModule, + ButtonModule, + DialogService, + FormFieldModule, + IconButtonModule, + ToastService, +} from "@bitwarden/components"; +import { BiometricStateService } from "@bitwarden/key-management"; + +import { PinServiceAbstraction } from "../../common/abstractions"; +import { AnonLayoutWrapperDataService } from "../anon-layout/anon-layout-wrapper-data.service"; + +import { + UnlockOption, + LockComponentService, + UnlockOptions, + UnlockOptionValue, +} from "./lock-component.service"; + +const BroadcasterSubscriptionId = "LockComponent"; + +const clientTypeToSuccessRouteRecord: Partial> = { + [ClientType.Web]: "vault", + [ClientType.Desktop]: "vault", + [ClientType.Browser]: "/tabs/current", +}; + +@Component({ + selector: "bit-lock", + templateUrl: "lock.component.html", + standalone: true, + imports: [ + CommonModule, + JslibModule, + ReactiveFormsModule, + ButtonModule, + FormFieldModule, + AsyncActionsModule, + IconButtonModule, + ], +}) +export class LockV2Component implements OnInit, OnDestroy { + private destroy$ = new Subject(); + + activeAccount: { id: UserId | undefined } & AccountInfo; + + clientType: ClientType; + ClientType = ClientType; + + unlockOptions: UnlockOptions = null; + + UnlockOption = UnlockOption; + + private _activeUnlockOptionBSubject: BehaviorSubject = + new BehaviorSubject(null); + + activeUnlockOption$ = this._activeUnlockOptionBSubject.asObservable(); + + set activeUnlockOption(value: UnlockOptionValue) { + this._activeUnlockOptionBSubject.next(value); + } + + get activeUnlockOption(): UnlockOptionValue { + return this._activeUnlockOptionBSubject.value; + } + + private invalidPinAttempts = 0; + + biometricUnlockBtnText: string; + + // masterPassword = ""; + showPassword = false; + private enforcedMasterPasswordOptions: MasterPasswordPolicyOptions = undefined; + + forcePasswordResetRoute = "update-temp-password"; + + formGroup: FormGroup; + + // Desktop properties: + private deferFocus: boolean = null; + private biometricAsked = false; + + // Browser extension properties: + private isInitialLockScreen = (window as any).previousPopupUrl == null; + + defaultUnlockOptionSetForUser = false; + + unlockingViaBiometrics = false; + + constructor( + private accountService: AccountService, + private pinService: PinServiceAbstraction, + private userVerificationService: UserVerificationService, + private cryptoService: CryptoService, + private platformUtilsService: PlatformUtilsService, + private router: Router, + private dialogService: DialogService, + private messagingService: MessagingService, + private biometricStateService: BiometricStateService, + private ngZone: NgZone, + private i18nService: I18nService, + private masterPasswordService: InternalMasterPasswordServiceAbstraction, + private logService: LogService, + private deviceTrustService: DeviceTrustServiceAbstraction, + private syncService: SyncService, + private policyService: InternalPolicyService, + private passwordStrengthService: PasswordStrengthServiceAbstraction, + private formBuilder: FormBuilder, + private toastService: ToastService, + + private lockComponentService: LockComponentService, + private anonLayoutWrapperDataService: AnonLayoutWrapperDataService, + + // desktop deps + private broadcasterService: BroadcasterService, + ) {} + + async ngOnInit() { + this.listenForActiveUnlockOptionChanges(); + + // Listen for active account changes + this.listenForActiveAccountChanges(); + + // Identify client + this.clientType = this.platformUtilsService.getClientType(); + + if (this.clientType === "desktop") { + await this.desktopOnInit(); + } + } + + // Base component methods + private listenForActiveUnlockOptionChanges() { + this.activeUnlockOption$ + .pipe(takeUntil(this.destroy$)) + .subscribe((activeUnlockOption: UnlockOptionValue) => { + if (activeUnlockOption === UnlockOption.Pin) { + this.buildPinForm(); + } else if (activeUnlockOption === UnlockOption.MasterPassword) { + this.buildMasterPasswordForm(); + } + }); + } + + private buildMasterPasswordForm() { + this.formGroup = this.formBuilder.group( + { + masterPassword: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private buildPinForm() { + this.formGroup = this.formBuilder.group( + { + pin: ["", [Validators.required]], + }, + { updateOn: "submit" }, + ); + } + + private listenForActiveAccountChanges() { + this.accountService.activeAccount$ + .pipe( + switchMap((account) => { + return this.handleActiveAccountChange(account); + }), + takeUntil(this.destroy$), + ) + .subscribe(); + } + + private async handleActiveAccountChange(activeAccount: { id: UserId | undefined } & AccountInfo) { + this.activeAccount = activeAccount; + + this.resetDataOnActiveAccountChange(); + + this.setEmailAsPageSubtitle(activeAccount.email); + + this.unlockOptions = await firstValueFrom( + this.lockComponentService.getAvailableUnlockOptions$(activeAccount.id), + ); + + this.setDefaultActiveUnlockOption(this.unlockOptions); + + if (this.unlockOptions.biometrics.enabled) { + await this.handleBiometricsUnlockEnabled(); + } + } + + private resetDataOnActiveAccountChange() { + this.defaultUnlockOptionSetForUser = false; + this.unlockOptions = null; + this.activeUnlockOption = null; + this.formGroup = null; // new form group will be created based on new active unlock option + + // Desktop properties: + this.biometricAsked = false; + } + + private setEmailAsPageSubtitle(email: string) { + this.anonLayoutWrapperDataService.setAnonLayoutWrapperData({ + pageSubtitle: { + subtitle: email, + translate: false, + }, + }); + } + + private setDefaultActiveUnlockOption(unlockOptions: UnlockOptions) { + // Priorities should be Biometrics > Pin > Master Password for speed + if (unlockOptions.biometrics.enabled) { + this.activeUnlockOption = UnlockOption.Biometrics; + } else if (unlockOptions.pin.enabled) { + this.activeUnlockOption = UnlockOption.Pin; + } else if (unlockOptions.masterPassword.enabled) { + this.activeUnlockOption = UnlockOption.MasterPassword; + } + } + + private async handleBiometricsUnlockEnabled() { + this.biometricUnlockBtnText = this.lockComponentService.getBiometricsUnlockBtnText(); + + const autoPromptBiometrics = await firstValueFrom( + this.biometricStateService.promptAutomatically$, + ); + + // TODO: PM-12546 - we need to make our biometric autoprompt experience consistent between the + // desktop and extension. + if (this.clientType === "desktop") { + if (autoPromptBiometrics) { + await this.desktopAutoPromptBiometrics(); + } + } + + if (this.clientType === "browser") { + if ( + this.unlockOptions.biometrics.enabled && + autoPromptBiometrics && + this.isInitialLockScreen // only autoprompt biometrics on initial lock screen + ) { + await this.unlockViaBiometrics(); + } + } + } + + // Note: this submit method is only used for unlock methods that require a form and user input. + // For biometrics unlock, the method is called directly. + submit = async (): Promise => { + if (this.activeUnlockOption === UnlockOption.Pin) { + return await this.unlockViaPin(); + } + + await this.unlockViaMasterPassword(); + }; + + async logOut() { + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "logOut" }, + content: { key: "logOutConfirmation" }, + acceptButtonText: { key: "logOut" }, + type: "warning", + }); + + if (confirmed) { + this.messagingService.send("logout", { userId: this.activeAccount.id }); + } + } + + async unlockViaBiometrics(): Promise { + this.unlockingViaBiometrics = true; + + if (!this.unlockOptions.biometrics.enabled) { + this.unlockingViaBiometrics = false; + return; + } + + try { + await this.biometricStateService.setUserPromptCancelled(); + const userKey = await this.cryptoService.getUserKeyFromStorage( + KeySuffixOptions.Biometric, + this.activeAccount.id, + ); + + // If user cancels biometric prompt, userKey is undefined. + if (userKey) { + await this.setUserKeyAndContinue(userKey, false); + } + + this.unlockingViaBiometrics = false; + } catch (e) { + // Cancelling is a valid action. + if (e?.message === "canceled") { + this.unlockingViaBiometrics = false; + return; + } + + let biometricTranslatedErrorDesc; + + if (this.clientType === "browser") { + const biometricErrorDescTranslationKey = this.lockComponentService.getBiometricsError(e); + + if (biometricErrorDescTranslationKey) { + biometricTranslatedErrorDesc = this.i18nService.t(biometricErrorDescTranslationKey); + } + } + + // if no translation key found, show generic error message + if (!biometricTranslatedErrorDesc) { + biometricTranslatedErrorDesc = this.i18nService.t("unexpectedError"); + } + + const confirmed = await this.dialogService.openSimpleDialog({ + title: { key: "error" }, + content: biometricTranslatedErrorDesc, + acceptButtonText: { key: "tryAgain" }, + type: "danger", + }); + + if (confirmed) { + // try again + await this.unlockViaBiometrics(); + } + + this.unlockingViaBiometrics = false; + } + } + + togglePassword() { + this.showPassword = !this.showPassword; + const input = document.getElementById( + this.unlockOptions.pin.enabled ? "pin" : "masterPassword", + ); + if (this.ngZone.isStable) { + input.focus(); + } else { + // eslint-disable-next-line rxjs-angular/prefer-takeuntil + this.ngZone.onStable.pipe(take(1)).subscribe(() => input.focus()); + } + } + + private validatePin(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("pinRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaPin() { + if (!this.validatePin()) { + return; + } + + const pin = this.formGroup.controls.pin.value; + + const MAX_INVALID_PIN_ENTRY_ATTEMPTS = 5; + + try { + const userKey = await this.pinService.decryptUserKeyWithPin(pin, this.activeAccount.id); + + if (userKey) { + await this.setUserKeyAndContinue(userKey); + return; // successfully unlocked + } + + // Failure state: invalid PIN or failed decryption + this.invalidPinAttempts++; + + // Log user out if they have entered an invalid PIN too many times + if (this.invalidPinAttempts >= MAX_INVALID_PIN_ENTRY_ATTEMPTS) { + this.toastService.showToast({ + variant: "error", + title: null, + message: this.i18nService.t("tooManyInvalidPinEntryAttemptsLoggingOut"), + }); + this.messagingService.send("logout"); + return; + } + + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidPin"), + }); + } catch { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("unexpectedError"), + }); + } + } + + private validateMasterPassword(): boolean { + if (this.formGroup.invalid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("masterPasswordRequired"), + }); + return false; + } + + return true; + } + + private async unlockViaMasterPassword() { + if (!this.validateMasterPassword()) { + return; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const verification = { + type: VerificationType.MasterPassword, + secret: masterPassword, + } as MasterPasswordVerification; + + let passwordValid = false; + let masterPasswordVerificationResponse: MasterPasswordVerificationResponse; + try { + masterPasswordVerificationResponse = + await this.userVerificationService.verifyUserByMasterPassword( + verification, + this.activeAccount.id, + this.activeAccount.email, + ); + + this.enforcedMasterPasswordOptions = MasterPasswordPolicyOptions.fromResponse( + masterPasswordVerificationResponse.policyOptions, + ); + passwordValid = true; + } catch (e) { + this.logService.error(e); + } + + if (!passwordValid) { + this.toastService.showToast({ + variant: "error", + title: this.i18nService.t("errorOccurred"), + message: this.i18nService.t("invalidMasterPassword"), + }); + return; + } + + const userKey = await this.masterPasswordService.decryptUserKeyWithMasterKey( + masterPasswordVerificationResponse.masterKey, + ); + await this.setUserKeyAndContinue(userKey, true); + } + + private async setUserKeyAndContinue(key: UserKey, evaluatePasswordAfterUnlock = false) { + await this.cryptoService.setUserKey(key, this.activeAccount.id); + + // Now that we have a decrypted user key in memory, we can check if we + // need to establish trust on the current device + await this.deviceTrustService.trustDeviceIfRequired(this.activeAccount.id); + + await this.doContinue(evaluatePasswordAfterUnlock); + } + + private async doContinue(evaluatePasswordAfterUnlock: boolean) { + await this.biometricStateService.resetUserPromptCancelled(); + this.messagingService.send("unlocked"); + + if (evaluatePasswordAfterUnlock) { + try { + // If we do not have any saved policies, attempt to load them from the service + if (this.enforcedMasterPasswordOptions == undefined) { + this.enforcedMasterPasswordOptions = await firstValueFrom( + this.policyService.masterPasswordPolicyOptions$(), + ); + } + + if (this.requirePasswordChange()) { + const userId = (await firstValueFrom(this.accountService.activeAccount$))?.id; + await this.masterPasswordService.setForceSetPasswordReason( + ForceSetPasswordReason.WeakMasterPassword, + userId, + ); + await this.router.navigate([this.forcePasswordResetRoute]); + return; + } + } catch (e) { + // Do not prevent unlock if there is an error evaluating policies + this.logService.error(e); + } + } + + // Vault can be de-synced since notifications get ignored while locked. Need to check whether sync is required using the sync service. + await this.syncService.fullSync(false); + + if (this.clientType === "browser") { + const previousUrl = this.lockComponentService.getPreviousUrl(); + if (previousUrl) { + await this.router.navigateByUrl(previousUrl); + } + } + + // determine success route based on client type + const successRoute = clientTypeToSuccessRouteRecord[this.clientType]; + await this.router.navigate([successRoute]); + } + + /** + * Checks if the master password meets the enforced policy requirements + * If not, returns false + */ + private requirePasswordChange(): boolean { + if ( + this.enforcedMasterPasswordOptions == undefined || + !this.enforcedMasterPasswordOptions.enforceOnLogin + ) { + return false; + } + + const masterPassword = this.formGroup.controls.masterPassword.value; + + const passwordStrength = this.passwordStrengthService.getPasswordStrength( + masterPassword, + this.activeAccount.email, + )?.score; + + return !this.policyService.evaluateMasterPassword( + passwordStrength, + masterPassword, + this.enforcedMasterPasswordOptions, + ); + } + + // ----------------------------------------------------------------------------------------------- + // Desktop methods: + // ----------------------------------------------------------------------------------------------- + + async desktopOnInit() { + // TODO: move this into a WindowService and subscribe to messages via MessageListener service. + this.broadcasterService.subscribe(BroadcasterSubscriptionId, async (message: any) => { + this.ngZone.run(() => { + switch (message.command) { + case "windowHidden": + this.onWindowHidden(); + break; + case "windowIsFocused": + if (this.deferFocus === null) { + this.deferFocus = !message.windowIsFocused; + if (!this.deferFocus) { + this.focusInput(); + } + } else if (this.deferFocus && message.windowIsFocused) { + this.focusInput(); + this.deferFocus = false; + } + break; + default: + } + }); + }); + this.messagingService.send("getWindowIsFocused"); + } + + private async desktopAutoPromptBiometrics() { + if (!this.unlockOptions?.biometrics?.enabled || this.biometricAsked) { + return; + } + + // prevent the biometric prompt from showing if the user has already cancelled it + if (await firstValueFrom(this.biometricStateService.promptCancelled$)) { + return; + } + + const windowVisible = await this.lockComponentService.isWindowVisible(); + + if (windowVisible) { + this.biometricAsked = true; + await this.unlockViaBiometrics(); + } + } + + onWindowHidden() { + this.showPassword = false; + } + + private focusInput() { + if (this.unlockOptions) { + document.getElementById(this.unlockOptions.pin.enabled ? "pin" : "masterPassword")?.focus(); + } + } + + // ----------------------------------------------------------------------------------------------- + + ngOnDestroy() { + this.destroy$.next(); + this.destroy$.complete(); + + if (this.clientType === "desktop") { + this.broadcasterService.unsubscribe(BroadcasterSubscriptionId); + } + } +} From 97a97c4b2d8183dde02391d5f0aa8de8617b15aa Mon Sep 17 00:00:00 2001 From: Jordan Aasen <166539328+jaasen-livefront@users.noreply.github.com> Date: Tue, 1 Oct 2024 13:38:27 -0700 Subject: [PATCH 3/8] disable copy button if no password is present (#11349) --- .../src/send-form/components/options/send-options.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html index dbd86d7f5b7..adbca181947 100644 --- a/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html +++ b/libs/tools/send/send-ui/src/send-form/components/options/send-options.component.html @@ -35,6 +35,7 @@ bitIconButton="bwi-clone" bitSuffix [appA11yTitle]="'copyPassword' | i18n" + [disabled]="!sendOptionsForm.get('password').value" [valueLabel]="'password' | i18n" [appCopyClick]="sendOptionsForm.get('password').value" showToast From 8b034cda7db8cb89014ffcbeaf6891733eb0dff6 Mon Sep 17 00:00:00 2001 From: cyprain-okeke <108260115+cyprain-okeke@users.noreply.github.com> Date: Tue, 1 Oct 2024 22:01:01 +0100 Subject: [PATCH 4/8] Remove the delete provider flag (#11336) --- .../admin-console/providers/settings/account.component.html | 2 +- .../app/admin-console/providers/settings/account.component.ts | 4 ---- libs/common/src/enums/feature-flag.enum.ts | 2 -- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html index a4e45877552..b6794b2987f 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.html @@ -34,7 +34,7 @@ - + diff --git a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts index d5d7634db4c..0442f04fb72 100644 --- a/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts +++ b/bitwarden_license/bit-web/src/app/admin-console/providers/settings/account.component.ts @@ -8,7 +8,6 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ProviderApiServiceAbstraction } from "@bitwarden/common/admin-console/abstractions/provider/provider-api.service.abstraction"; import { ProviderUpdateRequest } from "@bitwarden/common/admin-console/models/request/provider/provider-update.request"; import { ProviderResponse } from "@bitwarden/common/admin-console/models/response/provider/provider.response"; -import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; @@ -33,9 +32,6 @@ export class AccountComponent implements OnDestroy, OnInit { providerName: ["" as ProviderResponse["name"]], providerBillingEmail: ["" as ProviderResponse["billingEmail"], Validators.email], }); - protected enableDeleteProvider$ = this.configService.getFeatureFlag$( - FeatureFlag.EnableDeleteProvider, - ); constructor( private apiService: ApiService, diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index 2b7d2bea334..f8967212b20 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -9,7 +9,6 @@ export enum FeatureFlag { GeneratorToolsModernization = "generator-tools-modernization", EnableConsolidatedBilling = "enable-consolidated-billing", AC1795_UpdatedSubscriptionStatusSection = "AC-1795_updated-subscription-status-section", - EnableDeleteProvider = "AC-1218-delete-provider", ExtensionRefresh = "extension-refresh", PersistPopupView = "persist-popup-view", PM4154_BulkEncryptionService = "PM-4154-bulk-encryption-service", @@ -54,7 +53,6 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.GeneratorToolsModernization]: FALSE, [FeatureFlag.EnableConsolidatedBilling]: FALSE, [FeatureFlag.AC1795_UpdatedSubscriptionStatusSection]: FALSE, - [FeatureFlag.EnableDeleteProvider]: FALSE, [FeatureFlag.ExtensionRefresh]: FALSE, [FeatureFlag.PersistPopupView]: FALSE, [FeatureFlag.PM4154_BulkEncryptionService]: FALSE, From 363acf58f97e9ce4c8682ead94ac5b8269f7e304 Mon Sep 17 00:00:00 2001 From: Thomas Rittson <31796059+eliykat@users.noreply.github.com> Date: Wed, 2 Oct 2024 08:07:13 +1000 Subject: [PATCH 5/8] [PM-12740] Move CollectionAdminService to AC Team (#11269) --- .../organizations/core/views/group.view.ts | 3 +-- .../organizations/core/views/index.ts | 1 - .../core/views/organization-user-admin-view.ts | 3 +-- .../core/views/organization-user.view.ts | 7 ++++--- .../manage/group-add-edit.component.ts | 8 +++++--- .../member-dialog/member-dialog.component.ts | 12 ++++++------ .../settings/org-import.component.ts | 2 +- .../access-selector/access-selector.models.ts | 7 +++++-- apps/web/src/app/core/core.module.ts | 14 +++++++++++--- .../import/import-collection-admin.service.ts | 4 ++-- .../collection-dialog.component.ts | 11 ++++------- .../vault-collection-row.component.ts | 3 +-- .../vault-items/vault-items.component.ts | 2 +- .../vault-items/vault-items.stories.ts | 12 ++++++------ .../organization-name-badge.component.ts | 3 +-- .../abstractions/vault-filter.service.ts | 2 +- .../routed-vault-filter-bridge.service.ts | 7 ++----- .../services/vault-filter.service.ts | 2 +- .../shared/models/filter-function.spec.ts | 3 ++- .../shared/models/filter-function.ts | 3 ++- .../models/routed-vault-filter-bridge.model.ts | 2 +- .../shared/models/routed-vault-filter.model.ts | 2 -- .../shared/models/vault-filter.type.ts | 3 +-- .../vault-header/vault-header.component.ts | 2 +- .../vault/individual-vault/vault.component.ts | 2 +- .../bulk-collections-dialog.component.ts | 6 ++++-- .../vault-filter/vault-filter.service.ts | 2 +- .../vault-header/vault-header.component.ts | 8 +++++--- .../src/app/vault/org-vault/vault.component.ts | 8 +++++--- apps/web/src/app/vault/utils/collection-utils.ts | 3 +-- .../services/member-access-report.service.ts | 2 +- .../abstractions/collection-admin.service.ts | 16 ++++++++++++++++ .../src/common/collections/abstractions/index.ts | 1 + .../src/common/collections/index.ts | 3 +++ .../models}/bulk-collection-access.request.ts | 0 .../models}/collection-access-selection.view.ts | 0 .../collections/models}/collection-admin.view.ts | 5 +++-- .../src/common/collections/models/index.ts | 3 +++ .../services/default-collection-admin.service.ts | 15 +++++++-------- .../src/common/collections/services/index.ts | 1 + libs/admin-console/src/common/index.ts | 1 + 41 files changed, 113 insertions(+), 81 deletions(-) create mode 100644 libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts create mode 100644 libs/admin-console/src/common/collections/abstractions/index.ts create mode 100644 libs/admin-console/src/common/collections/index.ts rename {apps/web/src/app/vault/core => libs/admin-console/src/common/collections/models}/bulk-collection-access.request.ts (100%) rename {apps/web/src/app/admin-console/organizations/core/views => libs/admin-console/src/common/collections/models}/collection-access-selection.view.ts (100%) rename {apps/web/src/app/vault/core/views => libs/admin-console/src/common/collections/models}/collection-admin.view.ts (92%) create mode 100644 libs/admin-console/src/common/collections/models/index.ts rename apps/web/src/app/vault/core/collection-admin.service.ts => libs/admin-console/src/common/collections/services/default-collection-admin.service.ts (94%) create mode 100644 libs/admin-console/src/common/collections/services/index.ts diff --git a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts index 1909b9a863c..67ce47c624a 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/group.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/group.view.ts @@ -1,9 +1,8 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { View } from "@bitwarden/common/src/models/view/view"; import { GroupDetailsResponse, GroupResponse } from "../services/group/responses/group.response"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class GroupView implements View { id: string; organizationId: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/index.ts b/apps/web/src/app/admin-console/organizations/core/views/index.ts index ef14753c48a..9408d7757c3 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/index.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/index.ts @@ -1,4 +1,3 @@ -export * from "./collection-access-selection.view"; export * from "./group.view"; export * from "./organization-user.view"; export * from "./organization-user-admin-view"; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts index 97e77d8543c..b9b034b405d 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user-admin-view.ts @@ -1,11 +1,10 @@ +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class OrganizationUserAdminView { id: string; userId: string; diff --git a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts index 8988f41487c..7d1a10c5332 100644 --- a/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts +++ b/apps/web/src/app/admin-console/organizations/core/views/organization-user.view.ts @@ -1,12 +1,13 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; +import { + OrganizationUserUserDetailsResponse, + CollectionAccessSelectionView, +} from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; -import { CollectionAccessSelectionView } from "./collection-access-selection.view"; - export class OrganizationUserView { id: string; userId: string; diff --git a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts index cdbc049111d..643e76e4c38 100644 --- a/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts +++ b/apps/web/src/app/admin-console/organizations/manage/group-add-edit.component.ts @@ -14,7 +14,11 @@ import { takeUntil, } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + CollectionAdminView, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -26,8 +30,6 @@ import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/pl import { UserId } from "@bitwarden/common/types/guid"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { InternalGroupService as GroupService, GroupView } from "../core"; import { AccessItemType, diff --git a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts index fb11ad21c4c..aac096189e0 100644 --- a/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts +++ b/apps/web/src/app/admin-console/organizations/members/components/member-dialog/member-dialog.component.ts @@ -13,7 +13,12 @@ import { takeUntil, } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAccessSelectionView, + CollectionAdminService, + CollectionAdminView, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationUserStatusType, @@ -24,14 +29,10 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { DialogService, ToastService } from "@bitwarden/components"; -import { CollectionAdminService } from "../../../../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../../../../vault/core/views/collection-admin.view"; import { - CollectionAccessSelectionView, GroupService, GroupView, OrganizationUserAdminView, @@ -133,7 +134,6 @@ export class MemberDialogComponent implements OnDestroy { @Inject(DIALOG_DATA) protected params: MemberDialogParams, private dialogRef: DialogRef, private i18nService: I18nService, - private platformUtilsService: PlatformUtilsService, private formBuilder: FormBuilder, // TODO: We should really look into consolidating naming conventions for these services private collectionAdminService: CollectionAdminService, diff --git a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts index 36cfd4230c7..2c2d700fe80 100644 --- a/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts +++ b/apps/web/src/app/admin-console/organizations/settings/org-import.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { CollectionAdminService } from "@bitwarden/admin-console/common"; import { canAccessVaultTab, OrganizationService, @@ -11,7 +12,6 @@ import { ImportComponent } from "@bitwarden/importer/ui"; import { LooseComponentsModule, SharedModule } from "../../../shared"; import { ImportCollectionAdminService } from "../../../tools/import/import-collection-admin.service"; -import { CollectionAdminService } from "../../../vault/core/collection-admin.service"; @Component({ templateUrl: "org-import.component.html", diff --git a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts index 429b62ed0cc..1dc20366942 100644 --- a/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts +++ b/apps/web/src/app/admin-console/organizations/shared/components/access-selector/access-selector.models.ts @@ -1,11 +1,14 @@ -import { OrganizationUserUserDetailsResponse } from "@bitwarden/admin-console/common"; +import { + CollectionAccessSelectionView, + OrganizationUserUserDetailsResponse, +} from "@bitwarden/admin-console/common"; import { OrganizationUserStatusType, OrganizationUserType, } from "@bitwarden/common/admin-console/enums"; import { SelectItemView } from "@bitwarden/components"; -import { CollectionAccessSelectionView, GroupView } from "../../../core"; +import { GroupView } from "../../../core"; /** * Permission options that replace/correspond with manage, readOnly, and hidePassword server fields. diff --git a/apps/web/src/app/core/core.module.ts b/apps/web/src/app/core/core.module.ts index c14c9750474..37ce80a826d 100644 --- a/apps/web/src/app/core/core.module.ts +++ b/apps/web/src/app/core/core.module.ts @@ -1,7 +1,11 @@ import { CommonModule } from "@angular/common"; import { APP_INITIALIZER, NgModule, Optional, SkipSelf } from "@angular/core"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + DefaultCollectionAdminService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { SafeProvider, safeProvider } from "@bitwarden/angular/platform/utils/safe-provider"; import { CLIENT_TYPE, @@ -60,6 +64,7 @@ import { ThemeStateService, } from "@bitwarden/common/platform/theming/theme-state.service"; import { VaultTimeout, VaultTimeoutStringType } from "@bitwarden/common/types/vault-timeout.type"; +import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service"; import { BiometricsService } from "@bitwarden/key-management"; import { PolicyListService } from "../admin-console/core/policy-list.service"; @@ -75,7 +80,6 @@ import { WebBiometricsService } from "../key-management/web-biometric.service"; import { WebEnvironmentService } from "../platform/web-environment.service"; import { WebMigrationRunner } from "../platform/web-migration-runner"; import { WebStorageServiceProvider } from "../platform/web-storage-service.provider"; -import { CollectionAdminService } from "../vault/core/collection-admin.service"; import { EventService } from "./event.service"; import { InitService } from "./init.service"; @@ -149,7 +153,6 @@ const safeProviders: SafeProvider[] = [ useClass: WebFileDownloadService, useAngularDecorators: true, }), - safeProvider(CollectionAdminService), safeProvider({ provide: WindowStorageService, useFactory: () => new WindowStorageService(window.localStorage), @@ -227,6 +230,11 @@ const safeProviders: SafeProvider[] = [ useClass: DefaultAppIdService, deps: [OBSERVABLE_DISK_LOCAL_STORAGE, LogService], }), + safeProvider({ + provide: CollectionAdminService, + useClass: DefaultCollectionAdminService, + deps: [ApiService, CryptoServiceAbstraction, EncryptService, CollectionService], + }), ]; @NgModule({ diff --git a/apps/web/src/app/tools/import/import-collection-admin.service.ts b/apps/web/src/app/tools/import/import-collection-admin.service.ts index e48f9b27ce6..093bfed7024 100644 --- a/apps/web/src/app/tools/import/import-collection-admin.service.ts +++ b/apps/web/src/app/tools/import/import-collection-admin.service.ts @@ -1,8 +1,8 @@ import { Injectable } from "@angular/core"; +import { CollectionAdminService, CollectionAdminView } from "@bitwarden/admin-console/common"; + import { ImportCollectionServiceAbstraction } from "../../../../../../libs/importer/src/services/import-collection.service.abstraction"; -import { CollectionAdminService } from "../../vault/core/collection-admin.service"; -import { CollectionAdminView } from "../../vault/core/views/collection-admin.view"; @Injectable() export class ImportCollectionAdminService implements ImportCollectionServiceAbstraction { diff --git a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts index 5c46a7a0296..821765ba41b 100644 --- a/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts +++ b/apps/web/src/app/vault/components/collection-dialog/collection-dialog.component.ts @@ -14,6 +14,9 @@ import { import { first } from "rxjs/operators"; import { + CollectionAccessSelectionView, + CollectionAdminService, + CollectionAdminView, OrganizationUserApiService, OrganizationUserUserMiniResponse, } from "@bitwarden/admin-console/common"; @@ -26,11 +29,7 @@ import { CollectionResponse } from "@bitwarden/common/vault/models/response/coll import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { BitValidators, DialogService } from "@bitwarden/components"; -import { - CollectionAccessSelectionView, - GroupService, - GroupView, -} from "../../../admin-console/organizations/core"; +import { GroupService, GroupView } from "../../../admin-console/organizations/core"; import { PermissionMode } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.component"; import { AccessItemType, @@ -40,8 +39,6 @@ import { convertToPermission, convertToSelectionView, } from "../../../admin-console/organizations/shared/components/access-selector/access-selector.models"; -import { CollectionAdminService } from "../../core/collection-admin.service"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; export enum CollectionDialogTabType { Info = 0, diff --git a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts index 09e7484b673..ec38c53480a 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-collection-row.component.ts @@ -1,12 +1,11 @@ import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { CollectionAdminView, Unassigned } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { GroupView } from "../../../admin-console/organizations/core"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { convertToPermission, diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts index 6ac19b75655..591c132e1e6 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.component.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.component.ts @@ -1,13 +1,13 @@ import { SelectionModel } from "@angular/cdk/collections"; import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; import { TableDataSource } from "@bitwarden/components"; import { GroupView } from "../../../admin-console/organizations/core"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { VaultItem } from "./vault-item"; import { VaultItemEvent } from "./vault-item-event"; diff --git a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts index bf0fa3aaef4..96089d2b156 100644 --- a/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts +++ b/apps/web/src/app/vault/components/vault-items/vault-items.stories.ts @@ -3,6 +3,11 @@ import { RouterModule } from "@angular/router"; import { applicationConfig, Meta, moduleMetadata, StoryObj } from "@storybook/angular"; import { BehaviorSubject, of } from "rxjs"; +import { + CollectionAccessSelectionView, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { OrganizationUserType } from "@bitwarden/common/admin-console/enums"; import { PermissionsApi } from "@bitwarden/common/admin-console/models/api/permissions.api"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -19,13 +24,8 @@ import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { - CollectionAccessSelectionView, - GroupView, -} from "../../../admin-console/organizations/core"; +import { GroupView } from "../../../admin-console/organizations/core"; import { PreloadedEnglishI18nModule } from "../../../core/tests"; -import { CollectionAdminView } from "../../core/views/collection-admin.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { VaultItemsComponent } from "./vault-items.component"; import { VaultItemsModule } from "./vault-items.module"; diff --git a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts index 6d53b8ad720..3e37d4998de 100644 --- a/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts +++ b/apps/web/src/app/vault/individual-vault/organization-badge/organization-name-badge.component.ts @@ -1,13 +1,12 @@ import { Component, Input, OnChanges } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { AvatarService } from "@bitwarden/common/auth/abstractions/avatar.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { Unassigned } from "../vault-filter/shared/models/routed-vault-filter.model"; - @Component({ selector: "app-org-badge", templateUrl: "organization-name-badge.component.html", diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts index 836cba22016..b18ee76e9c8 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/abstractions/vault-filter.service.ts @@ -1,11 +1,11 @@ import { Observable } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { CollectionView } from "@bitwarden/common/src/vault/models/view/collection.view"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts index 08ce9b67ba5..1f0a9e135b5 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/routed-vault-filter-bridge.service.ts @@ -2,15 +2,12 @@ import { Injectable } from "@angular/core"; import { Router } from "@angular/router"; import { combineLatest, map, Observable } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { RoutedVaultFilterBridge } from "../shared/models/routed-vault-filter-bridge.model"; -import { - RoutedVaultFilterModel, - Unassigned, - All, -} from "../shared/models/routed-vault-filter.model"; +import { RoutedVaultFilterModel, All } from "../shared/models/routed-vault-filter.model"; import { VaultFilter } from "../shared/models/vault-filter.model"; import { CipherTypeFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts index ac20f86d0ee..d8abfb2f794 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.ts @@ -10,6 +10,7 @@ import { switchMap, } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; @@ -26,7 +27,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; import { COLLAPSED_GROUPINGS } from "@bitwarden/common/vault/services/key-state/collapsed-groupings.state"; -import { CollectionAdminView } from "../../../core/views/collection-admin.view"; import { CipherTypeFilter, CollectionFilter, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts index 786b2b1c7aa..397b7810606 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.spec.ts @@ -1,8 +1,9 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { createFilterFunction } from "./filter-function"; -import { Unassigned, All } from "./routed-vault-filter.model"; +import { All } from "./routed-vault-filter.model"; describe("createFilter", () => { describe("given a generic cipher", () => { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts index 4716eb631b1..4b038512581 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/filter-function.ts @@ -1,7 +1,8 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { All, RoutedVaultFilterModel, Unassigned } from "./routed-vault-filter.model"; +import { All, RoutedVaultFilterModel } from "./routed-vault-filter.model"; export type FilterFunction = (cipher: CipherView) => boolean; diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts index 2f6047b6bbc..fe236a089e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter-bridge.model.ts @@ -1,3 +1,4 @@ +import { Unassigned } from "@bitwarden/admin-console/common"; import { CipherType } from "@bitwarden/common/vault/enums"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -8,7 +9,6 @@ import { isRoutedVaultFilterItemType, RoutedVaultFilterItemType, RoutedVaultFilterModel, - Unassigned, } from "./routed-vault-filter.model"; import { VaultFilter, VaultFilterFunction } from "./vault-filter.model"; import { diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts index 5579c62d4e9..4f2659d6101 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/routed-vault-filter.model.ts @@ -1,5 +1,3 @@ -export const Unassigned = "unassigned"; - export const All = "all"; // TODO: Remove `All` when moving to vertical navigation. diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts index fd349069aaa..0cd385bd19d 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/shared/models/vault-filter.type.ts @@ -1,10 +1,9 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FolderView } from "@bitwarden/common/src/vault/models/view/folder.view"; import { CipherType } from "@bitwarden/common/vault/enums"; import { ITreeNodeObject } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../../core/views/collection-admin.view"; - export type CipherStatus = "all" | "favorites" | "trash" | CipherType; export type CipherTypeFilter = ITreeNodeObject & { type: CipherStatus; icon: string }; diff --git a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts index 44e523abe61..463a03091e0 100644 --- a/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault-header/vault-header.component.ts @@ -9,6 +9,7 @@ import { } from "@angular/core"; import { firstValueFrom } from "rxjs"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -26,7 +27,6 @@ import { PipesModule } from "../pipes/pipes.module"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/individual-vault/vault.component.ts b/apps/web/src/app/vault/individual-vault/vault.component.ts index 8ad9deaf2bc..c94294d37d7 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -28,6 +28,7 @@ import { tap, } from "rxjs/operators"; +import { Unassigned } from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -118,7 +119,6 @@ import { createFilterFunction } from "./vault-filter/shared/models/filter-functi import { All, RoutedVaultFilterModel, - Unassigned, } from "./vault-filter/shared/models/routed-vault-filter.model"; import { VaultFilter } from "./vault-filter/shared/models/vault-filter.model"; import { FolderFilter, OrganizationFilter } from "./vault-filter/shared/models/vault-filter.type"; diff --git a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts index c4b0d8bc2a2..2839a6ae607 100644 --- a/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts +++ b/apps/web/src/app/vault/org-vault/bulk-collections-dialog/bulk-collections-dialog.component.ts @@ -3,7 +3,10 @@ import { Component, Inject, OnDestroy } from "@angular/core"; import { FormBuilder } from "@angular/forms"; import { combineLatest, of, Subject, switchMap, takeUntil } from "rxjs"; -import { OrganizationUserApiService } from "@bitwarden/admin-console/common"; +import { + CollectionAdminService, + OrganizationUserApiService, +} from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -23,7 +26,6 @@ import { PermissionMode, } from "../../../admin-console/organizations/shared/components/access-selector"; import { SharedModule } from "../../../shared"; -import { CollectionAdminService } from "../../core/collection-admin.service"; export interface BulkCollectionsDialogParams { organizationId: string; diff --git a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts index c6d4ee590b8..f9717f19f1e 100644 --- a/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts +++ b/apps/web/src/app/vault/org-vault/vault-filter/vault-filter.service.ts @@ -1,6 +1,7 @@ import { Injectable, OnDestroy } from "@angular/core"; import { map, Observable, ReplaySubject, Subject } from "rxjs"; +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -10,7 +11,6 @@ import { CollectionService } from "@bitwarden/common/vault/abstractions/collecti import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { VaultFilterService as BaseVaultFilterService } from "../../individual-vault/vault-filter/services/vault-filter.service"; import { CollectionFilter } from "../../individual-vault/vault-filter/shared/models/vault-filter.type"; diff --git a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts index 429062917ad..a3d564a9a3a 100644 --- a/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts +++ b/apps/web/src/app/vault/org-vault/vault-header/vault-header.component.ts @@ -3,6 +3,11 @@ import { Component, EventEmitter, Input, OnInit, Output } from "@angular/core"; import { Router } from "@angular/router"; import { firstValueFrom } from "rxjs"; +import { + CollectionAdminService, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; @@ -22,13 +27,10 @@ import { import { HeaderModule } from "../../../layouts/header/header.module"; import { SharedModule } from "../../../shared"; -import { CollectionAdminView } from "../../../vault/core/views/collection-admin.view"; import { CollectionDialogTabType } from "../../components/collection-dialog"; -import { CollectionAdminService } from "../../core/collection-admin.service"; import { All, RoutedVaultFilterModel, - Unassigned, } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; @Component({ diff --git a/apps/web/src/app/vault/org-vault/vault.component.ts b/apps/web/src/app/vault/org-vault/vault.component.ts index 3120b54ed38..7118fea4d09 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -30,6 +30,11 @@ import { withLatestFrom, } from "rxjs/operators"; +import { + CollectionAdminService, + CollectionAdminView, + Unassigned, +} from "@bitwarden/admin-console/common"; import { SearchPipe } from "@bitwarden/angular/pipes/search.pipe"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -72,8 +77,6 @@ import { } from "../components/collection-dialog"; import { VaultItemEvent } from "../components/vault-items/vault-item-event"; import { VaultItemsModule } from "../components/vault-items/vault-items.module"; -import { CollectionAdminService } from "../core/collection-admin.service"; -import { CollectionAdminView } from "../core/views/collection-admin.view"; import { BulkDeleteDialogResult, openBulkDeleteDialog, @@ -85,7 +88,6 @@ import { createFilterFunction } from "../individual-vault/vault-filter/shared/mo import { All, RoutedVaultFilterModel, - Unassigned, } from "../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; import { openViewCipherDialog, diff --git a/apps/web/src/app/vault/utils/collection-utils.ts b/apps/web/src/app/vault/utils/collection-utils.ts index b035c40f9f5..2f93e46bed2 100644 --- a/apps/web/src/app/vault/utils/collection-utils.ts +++ b/apps/web/src/app/vault/utils/collection-utils.ts @@ -1,3 +1,4 @@ +import { CollectionAdminView } from "@bitwarden/admin-console/common"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CollectionView, @@ -5,8 +6,6 @@ import { } from "@bitwarden/common/vault/models/view/collection.view"; import { ServiceUtils } from "@bitwarden/common/vault/service-utils"; -import { CollectionAdminView } from "../../vault/core/views/collection-admin.view"; - export function getNestedCollectionTree( collections: CollectionAdminView[], ): TreeNode[]; diff --git a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts index 3616893e231..443edc1d2fc 100644 --- a/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts +++ b/bitwarden_license/bit-web/src/app/tools/reports/member-access-report/services/member-access-report.service.ts @@ -1,9 +1,9 @@ import { Injectable } from "@angular/core"; +import { CollectionAccessSelectionView } from "@bitwarden/admin-console/common"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { EncString } from "@bitwarden/common/platform/models/domain/enc-string"; import { OrganizationId } from "@bitwarden/common/types/guid"; -import { CollectionAccessSelectionView } from "@bitwarden/web-vault/app/admin-console/organizations/core/views"; import { getPermissionList, convertToPermission, diff --git a/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts new file mode 100644 index 00000000000..e5b0bde7ef6 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/collection-admin.service.ts @@ -0,0 +1,16 @@ +import { CollectionDetailsResponse } from "@bitwarden/common/vault/models/response/collection.response"; + +import { CollectionAccessSelectionView, CollectionAdminView } from "../models"; + +export abstract class CollectionAdminService { + getAll: (organizationId: string) => Promise; + get: (organizationId: string, collectionId: string) => Promise; + save: (collection: CollectionAdminView) => Promise; + delete: (organizationId: string, collectionId: string) => Promise; + bulkAssignAccess: ( + organizationId: string, + collectionIds: string[], + users: CollectionAccessSelectionView[], + groups: CollectionAccessSelectionView[], + ) => Promise; +} diff --git a/libs/admin-console/src/common/collections/abstractions/index.ts b/libs/admin-console/src/common/collections/abstractions/index.ts new file mode 100644 index 00000000000..4ee56102061 --- /dev/null +++ b/libs/admin-console/src/common/collections/abstractions/index.ts @@ -0,0 +1 @@ +export * from "./collection-admin.service"; diff --git a/libs/admin-console/src/common/collections/index.ts b/libs/admin-console/src/common/collections/index.ts new file mode 100644 index 00000000000..9187ccd39cf --- /dev/null +++ b/libs/admin-console/src/common/collections/index.ts @@ -0,0 +1,3 @@ +export * from "./abstractions"; +export * from "./models"; +export * from "./services"; diff --git a/apps/web/src/app/vault/core/bulk-collection-access.request.ts b/libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts similarity index 100% rename from apps/web/src/app/vault/core/bulk-collection-access.request.ts rename to libs/admin-console/src/common/collections/models/bulk-collection-access.request.ts diff --git a/apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts b/libs/admin-console/src/common/collections/models/collection-access-selection.view.ts similarity index 100% rename from apps/web/src/app/admin-console/organizations/core/views/collection-access-selection.view.ts rename to libs/admin-console/src/common/collections/models/collection-access-selection.view.ts diff --git a/apps/web/src/app/vault/core/views/collection-admin.view.ts b/libs/admin-console/src/common/collections/models/collection-admin.view.ts similarity index 92% rename from apps/web/src/app/vault/core/views/collection-admin.view.ts rename to libs/admin-console/src/common/collections/models/collection-admin.view.ts index 10f894505c9..208131a3f71 100644 --- a/apps/web/src/app/vault/core/views/collection-admin.view.ts +++ b/libs/admin-console/src/common/collections/models/collection-admin.view.ts @@ -2,8 +2,9 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { CollectionAccessDetailsResponse } from "@bitwarden/common/src/vault/models/response/collection.response"; import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view"; -import { CollectionAccessSelectionView } from "../../../admin-console/organizations/core/views/collection-access-selection.view"; -import { Unassigned } from "../../individual-vault/vault-filter/shared/models/routed-vault-filter.model"; +import { CollectionAccessSelectionView } from "../models"; + +export const Unassigned = "unassigned"; export class CollectionAdminView extends CollectionView { groups: CollectionAccessSelectionView[] = []; diff --git a/libs/admin-console/src/common/collections/models/index.ts b/libs/admin-console/src/common/collections/models/index.ts new file mode 100644 index 00000000000..4f35728b00a --- /dev/null +++ b/libs/admin-console/src/common/collections/models/index.ts @@ -0,0 +1,3 @@ +export * from "./bulk-collection-access.request"; +export * from "./collection-access-selection.view"; +export * from "./collection-admin.view"; diff --git a/apps/web/src/app/vault/core/collection-admin.service.ts b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts similarity index 94% rename from apps/web/src/app/vault/core/collection-admin.service.ts rename to libs/admin-console/src/common/collections/services/default-collection-admin.service.ts index e0c15e34047..aa2b5bb91d6 100644 --- a/apps/web/src/app/vault/core/collection-admin.service.ts +++ b/libs/admin-console/src/common/collections/services/default-collection-admin.service.ts @@ -1,5 +1,3 @@ -import { Injectable } from "@angular/core"; - import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service"; @@ -14,13 +12,14 @@ import { CollectionResponse, } from "@bitwarden/common/vault/models/response/collection.response"; -import { CollectionAccessSelectionView } from "../../admin-console/organizations/core"; +import { CollectionAdminService } from "../abstractions"; +import { + BulkCollectionAccessRequest, + CollectionAccessSelectionView, + CollectionAdminView, +} from "../models"; -import { BulkCollectionAccessRequest } from "./bulk-collection-access.request"; -import { CollectionAdminView } from "./views/collection-admin.view"; - -@Injectable() -export class CollectionAdminService { +export class DefaultCollectionAdminService implements CollectionAdminService { constructor( private apiService: ApiService, private cryptoService: CryptoService, diff --git a/libs/admin-console/src/common/collections/services/index.ts b/libs/admin-console/src/common/collections/services/index.ts new file mode 100644 index 00000000000..1e3ed96c6a0 --- /dev/null +++ b/libs/admin-console/src/common/collections/services/index.ts @@ -0,0 +1 @@ +export * from "./default-collection-admin.service"; diff --git a/libs/admin-console/src/common/index.ts b/libs/admin-console/src/common/index.ts index 0af54f8ffbf..edeff5aa314 100644 --- a/libs/admin-console/src/common/index.ts +++ b/libs/admin-console/src/common/index.ts @@ -1 +1,2 @@ export * from "./organization-user"; +export * from "./collections"; From 136776571290c20d384fcad2bdaef3731cbdf44c Mon Sep 17 00:00:00 2001 From: Jonas Hendrickx Date: Wed, 2 Oct 2024 11:23:40 +0200 Subject: [PATCH 6/8] [PM-11503] Organization Automatic Sync verbiage is misleading (#11151) --- .../billing-sync-api-key.component.html | 2 +- .../billing-sync-key.component.html | 2 +- ...nization-subscription-cloud.component.html | 2 +- ...ation-subscription-selfhost.component.html | 6 ++--- apps/web/src/locales/en/messages.json | 23 +++++++++++-------- 5 files changed, 19 insertions(+), 16 deletions(-) diff --git a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html index 8b5ef867cca..4857a43a1ca 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-api-key.component.html @@ -1,7 +1,7 @@

- {{ (hasBillingToken ? "viewBillingSyncToken" : "generateBillingSyncToken") | i18n }} + {{ (hasBillingToken ? "viewBillingToken" : "generateBillingToken") | i18n }}

diff --git a/apps/web/src/app/billing/organizations/billing-sync-key.component.html b/apps/web/src/app/billing/organizations/billing-sync-key.component.html index 808cd83ec67..5f6b8482875 100644 --- a/apps/web/src/app/billing/organizations/billing-sync-key.component.html +++ b/apps/web/src/app/billing/organizations/billing-sync-key.component.html @@ -1,7 +1,7 @@

- {{ "manageBillingSync" | i18n }} + {{ "manageBillingTokenSync" | i18n }}

{{ "billingSyncKeyDesc" | i18n }}

diff --git a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html index 341324c4a2a..643eeb93bad 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-cloud.component.html @@ -280,7 +280,7 @@ (click)="manageBillingSync()" *ngIf="canManageBillingSync" > - {{ (hasBillingSyncToken ? "manageBillingSync" : "setUpBillingSync") | i18n }} + {{ (hasBillingSyncToken ? "viewBillingToken" : "setUpBillingSync") | i18n }}
diff --git a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html index 0a029de79dc..5a1ccc0768a 100644 --- a/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html +++ b/apps/web/src/app/billing/organizations/organization-subscription-selfhost.component.html @@ -90,7 +90,7 @@ - {{ "billingSyncDesc" | i18n }} + {{ "automaticBillingSyncDesc" | i18n }} @@ -100,7 +100,7 @@ type="button" (click)="manageBillingSyncSelfHosted()" > - {{ "manageBillingSync" | i18n }} + {{ "manageBillingTokenSync" | i18n }}