From a2945203f49933ead622f8e96ad8335ee8217f32 Mon Sep 17 00:00:00 2001 From: SmithThe4th Date: Wed, 12 Feb 2025 08:53:31 -0500 Subject: [PATCH] [PM-12047] Remove usage of ActiveUserState from cipher.service (#12814) * Cipher service web changes * Updated browser client to pass user id to cipher service observable changes * Cli changes * desktop changes * Fixed test * Libs changes * Fixed merge conflicts * Fixed merge conflicts * removed duplicate reference fixed conflict * Fixed test * Fixed test * Fixed test * Fixed desturcturing issue on failed to decrypt ciphers cipher service * Updated abstraction to use method syntax * Fixed conflicts * Fixed test on add edit v2 Passed active userId to delete function * Used getUserId utility function * made vault changes * made suggestion changes * made suggestion changes * made suggestion changes * Replace getUserId function calls with pipe operator syntax for better consistency * fixed merge conflicts * revert mistake made of usinf account activity during merge conflict fix * fixed conflicts * fixed tests --- .../notification.background.spec.ts | 16 +- .../background/notification.background.ts | 91 ++++-- .../background/overlay.background.spec.ts | 7 +- .../autofill/background/overlay.background.ts | 41 ++- .../background/web-request.background.ts | 17 +- .../cipher-context-menu-handler.spec.ts | 21 +- .../browser/cipher-context-menu-handler.ts | 14 +- .../context-menu-clicked-handler.spec.ts | 16 +- .../browser/context-menu-clicked-handler.ts | 16 +- .../overlay.background.deprecated.spec.ts | 3 +- .../overlay.background.deprecated.ts | 26 +- .../autofill/popup/fido2/fido2.component.ts | 10 +- .../services/autofill.service.spec.ts | 23 +- .../src/autofill/services/autofill.service.ts | 32 +- .../browser/src/background/main.background.ts | 4 + .../src/platform/listeners/update-badge.ts | 13 +- apps/browser/src/popup/app.component.ts | 2 +- .../add-edit/add-edit-v2.component.spec.ts | 12 +- .../add-edit/add-edit-v2.component.ts | 24 +- .../assign-collections.component.ts | 26 +- .../open-attachments.component.ts | 2 +- .../vault-header-v2.component.spec.ts | 4 +- .../vault-list-items-container.component.ts | 6 +- ...ault-password-history-v2.component.spec.ts | 13 +- .../vault-password-history-v2.component.ts | 4 +- .../components/vault-v2/vault-v2.component.ts | 19 +- .../view-v2/view-v2.component.spec.ts | 2 +- .../vault-v2/view-v2/view-v2.component.ts | 33 +- .../vault-popup-items.service.spec.ts | 33 +- .../services/vault-popup-items.service.ts | 26 +- .../vault-popup-list-filters.service.spec.ts | 2 +- .../vault-popup-list-filters.service.ts | 28 +- .../trash-list-items-container.component.ts | 10 +- .../admin-console/commands/share.command.ts | 12 +- apps/cli/src/commands/edit.command.ts | 24 +- apps/cli/src/commands/get.command.ts | 19 +- apps/cli/src/commands/list.command.ts | 12 +- apps/cli/src/commands/restore.command.ts | 15 +- apps/cli/src/oss-serve-configurator.ts | 5 +- apps/cli/src/vault.program.ts | 5 +- apps/cli/src/vault/create.command.ts | 13 +- apps/cli/src/vault/delete.command.ts | 28 +- .../services/desktop-autofill.service.ts | 24 +- .../platform/services/ssh-agent.service.ts | 13 +- .../encrypted-message-handler.service.ts | 32 +- .../vault/app/vault/vault-items.component.ts | 4 +- .../src/vault/app/vault/vault.component.ts | 14 +- .../exposed-passwords-report.component.ts | 9 +- .../reused-passwords-report.component.ts | 9 +- .../tools/weak-passwords-report.component.ts | 10 +- .../settings/change-password.component.ts | 4 +- .../reports/pages/cipher-report.component.ts | 10 +- ...exposed-passwords-report.component.spec.ts | 3 +- ...active-two-factor-report.component.spec.ts | 3 +- .../reused-passwords-report.component.spec.ts | 4 +- ...nsecured-websites-report.component.spec.ts | 3 +- .../weak-passwords-report.component.spec.ts | 3 +- .../vault-item-dialog.component.ts | 22 +- .../bulk-delete-dialog.component.ts | 10 +- .../bulk-move-dialog.component.ts | 14 +- .../services/vault-filter.service.spec.ts | 2 +- .../services/vault-filter.service.ts | 2 +- .../vault/individual-vault/vault.component.ts | 36 ++- .../vault/individual-vault/view.component.ts | 5 +- .../app/vault/org-vault/add-edit.component.ts | 8 +- .../vault/org-vault/attachments.component.ts | 9 +- ...console-cipher-form-config.service.spec.ts | 19 +- ...dmin-console-cipher-form-config.service.ts | 4 +- .../app/vault/org-vault/vault.component.ts | 20 +- .../components/collections.component.ts | 19 +- .../angular/src/components/share.component.ts | 13 +- .../vault/components/add-edit.component.ts | 42 +-- .../vault/components/attachments.component.ts | 35 +- .../components/password-history.component.ts | 9 +- .../vault/components/vault-items.component.ts | 34 +- .../src/vault/components/view.component.ts | 28 +- .../services/vault-filter.service.ts | 19 +- .../internal/default-notifications.service.ts | 6 +- .../fido2/fido2-authenticator.service.spec.ts | 6 +- .../fido2/fido2-authenticator.service.ts | 18 +- .../src/platform/sync/core-sync.service.ts | 22 +- libs/common/src/platform/sync/sync.service.ts | 3 +- .../event/event-collection.service.ts | 11 +- .../src/vault/abstractions/cipher.service.ts | 162 ++++++---- .../src/vault/services/cipher.service.spec.ts | 12 +- .../src/vault/services/cipher.service.ts | 300 ++++++++++-------- .../vault/services/folder/folder.service.ts | 2 +- ...ser-asymmetric-key-regeneration.service.ts | 6 +- .../individual-vault-export.service.ts | 15 +- .../src/services/org-vault-export.service.ts | 13 +- .../cipher-attachments.component.spec.ts | 2 +- .../cipher-attachments.component.ts | 9 +- .../delete-attachment.component.spec.ts | 11 +- .../delete-attachment.component.ts | 18 +- .../default-cipher-form-config.service.ts | 11 +- .../services/default-cipher-form.service.ts | 16 +- .../autofill-options-view.component.ts | 7 +- .../assign-collections.component.ts | 25 +- 98 files changed, 1174 insertions(+), 725 deletions(-) diff --git a/apps/browser/src/autofill/background/notification.background.spec.ts b/apps/browser/src/autofill/background/notification.background.spec.ts index 37c05a55a3a..40c4d07cadf 100644 --- a/apps/browser/src/autofill/background/notification.background.spec.ts +++ b/apps/browser/src/autofill/background/notification.background.spec.ts @@ -825,6 +825,7 @@ describe("NotificationBackground", () => { queueMessage.newPassword, message.edit, sender.tab, + "testId", ); expect(updateWithServerSpy).toHaveBeenCalled(); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { @@ -862,6 +863,7 @@ describe("NotificationBackground", () => { queueMessage.password, message.edit, sender.tab, + "testId", ); expect(editItemSpy).not.toHaveBeenCalled(); expect(createWithServerSpy).not.toHaveBeenCalled(); @@ -895,6 +897,7 @@ describe("NotificationBackground", () => { queueMessage.newPassword, message.edit, sender.tab, + "testId", ); expect(editItemSpy).toHaveBeenCalled(); expect(updateWithServerSpy).not.toHaveBeenCalled(); @@ -904,10 +907,13 @@ describe("NotificationBackground", () => { expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "editedCipher", }); - expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({ - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }); + expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith( + { + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }, + "testId", + ); expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { cipherId: cipherView.id, }); @@ -945,7 +951,7 @@ describe("NotificationBackground", () => { queueMessage, message.folder, ); - expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab); + expect(editItemSpy).toHaveBeenCalledWith(cipherView, "testId", sender.tab); expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { command: "closeNotificationBar", }); diff --git a/apps/browser/src/autofill/background/notification.background.ts b/apps/browser/src/autofill/background/notification.background.ts index a091256b28d..1a99425b7de 100644 --- a/apps/browser/src/autofill/background/notification.background.ts +++ b/apps/browser/src/autofill/background/notification.background.ts @@ -1,12 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { ExtensionCommand, ExtensionCommandType, @@ -22,6 +23,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -87,8 +89,6 @@ export default class NotificationBackground { bgGetDecryptedCiphers: () => this.getNotificationCipherData(), }; - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( private autofillService: AutofillService, private cipherService: CipherService, @@ -151,7 +151,13 @@ export default class NotificationBackground { firstValueFrom(this.environmentService.environment$), ]); const iconsServerUrl = env.getIconsUrl(); - const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl(currentTab.url); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + const decryptedCiphers = await this.cipherService.getAllDecryptedForUrl( + currentTab.url, + activeUserId, + ); return decryptedCiphers.map((view) => { const { id, name, reprompt, favorite, login } = view; @@ -304,7 +310,14 @@ export default class NotificationBackground { return; } - const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(loginInfo.url, activeUserId); const usernameMatches = ciphers.filter( (c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername, ); @@ -382,7 +395,14 @@ export default class NotificationBackground { } let id: string = null; - const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(changeData.url, activeUserId); if (changeData.currentPassword != null) { const passwordMatches = ciphers.filter( (c) => c.login.password === changeData.currentPassword, @@ -535,37 +555,42 @@ export default class NotificationBackground { this.notificationQueue.splice(i, 1); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (queueMessage.type === NotificationQueueMessageType.ChangePassword) { - const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId); - await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab); + const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId); + await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab, activeUserId); return; } // If the vault was locked, check if a cipher needs updating instead of creating a new one if (queueMessage.wasVaultLocked) { - const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri); + const allCiphers = await this.cipherService.getAllDecryptedForUrl( + queueMessage.uri, + activeUserId, + ); const existingCipher = allCiphers.find( (c) => c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, ); if (existingCipher != null) { - await this.updatePassword(existingCipher, queueMessage.password, edit, tab); + await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId); return; } } - folderId = (await this.folderExists(folderId)) ? folderId : null; + folderId = (await this.folderExists(folderId, activeUserId)) ? folderId : null; const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId); if (edit) { - await this.editItem(newCipher, tab); + await this.editItem(newCipher, activeUserId, tab); await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); return; } - const activeUserId = await firstValueFrom(this.activeUserId$); - const cipher = await this.cipherService.encrypt(newCipher, activeUserId); try { await this.cipherService.createWithServer(cipher); @@ -588,24 +613,25 @@ export default class NotificationBackground { * @param newPassword - The new password to update the cipher with * @param edit - Identifies if the cipher should be edited or simply updated * @param tab - The tab that the message was sent from + * @param userId - The active account user ID */ private async updatePassword( cipherView: CipherView, newPassword: string, edit: boolean, tab: chrome.tabs.Tab, + userId: UserId, ) { cipherView.login.password = newPassword; if (edit) { - await this.editItem(cipherView, tab); + await this.editItem(cipherView, userId, tab); await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); await BrowserApi.tabSendMessage(tab, { command: "editedCipher" }); return; } - const activeUserId = await firstValueFrom(this.activeUserId$); - const cipher = await this.cipherService.encrypt(cipherView, activeUserId); + const cipher = await this.cipherService.encrypt(cipherView, userId); try { // We've only updated the password, no need to broadcast editedCipher message await this.cipherService.updateWithServer(cipher); @@ -622,33 +648,34 @@ export default class NotificationBackground { * and opens the add/edit vault item popout. * * @param cipherView - The cipher to edit + * @param userId - The active account user ID * @param senderTab - The tab that the message was sent from */ - private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { - await this.cipherService.setAddEditCipherInfo({ - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }); + private async editItem(cipherView: CipherView, userId: UserId, senderTab: chrome.tabs.Tab) { + await this.cipherService.setAddEditCipherInfo( + { + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }, + userId, + ); await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); } - private async folderExists(folderId: string) { + private async folderExists(folderId: string, userId: UserId) { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") { return false; } - const activeUserId = await firstValueFrom(this.activeUserId$); - const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId)); + const folders = await firstValueFrom(this.folderService.folderViews$(userId)); return folders.some((x) => x.id === folderId); } - private async getDecryptedCipherById(cipherId: string) { - const cipher = await this.cipherService.get(cipherId); + private async getDecryptedCipherById(cipherId: string, userId: UserId) { + const cipher = await this.cipherService.get(cipherId, userId); if (cipher != null && cipher.type === CipherType.Login) { - const activeUserId = await firstValueFrom(this.activeUserId$); - return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), ); } return null; @@ -685,7 +712,9 @@ export default class NotificationBackground { * Returns the first value found from the folder service's folderViews$ observable. */ private async getFolderData() { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); return await firstValueFrom(this.folderService.folderViews$(activeUserId)); } diff --git a/apps/browser/src/autofill/background/overlay.background.spec.ts b/apps/browser/src/autofill/background/overlay.background.spec.ts index c3a6357ed05..22531788d37 100644 --- a/apps/browser/src/autofill/background/overlay.background.spec.ts +++ b/apps/browser/src/autofill/background/overlay.background.spec.ts @@ -206,6 +206,7 @@ describe("OverlayBackground", () => { inlineMenuFieldQualificationService, themeStateService, totpService, + accountService, generatedPasswordCallbackMock, addPasswordCallbackMock, ); @@ -849,7 +850,7 @@ describe("OverlayBackground", () => { await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [ CipherType.Card, CipherType.Identity, ]); @@ -872,7 +873,7 @@ describe("OverlayBackground", () => { await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId); expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( new Map([ @@ -891,7 +892,7 @@ describe("OverlayBackground", () => { await flushPromises(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [ CipherType.Card, CipherType.Identity, ]); diff --git a/apps/browser/src/autofill/background/overlay.background.ts b/apps/browser/src/autofill/background/overlay.background.ts index 3d2b1ec783c..1d55a154ee3 100644 --- a/apps/browser/src/autofill/background/overlay.background.ts +++ b/apps/browser/src/autofill/background/overlay.background.ts @@ -13,8 +13,10 @@ import { } from "rxjs"; import { parse } from "tldts"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service"; import { AutofillOverlayVisibility, SHOW_AUTOFILL_BUTTON, @@ -34,6 +36,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; @@ -225,6 +228,7 @@ export class OverlayBackground implements OverlayBackgroundInterface { private inlineMenuFieldQualificationService: InlineMenuFieldQualificationService, private themeStateService: ThemeStateService, private totpService: TotpService, + private accountService: AccountService, private generatePasswordCallback: () => Promise, private addPasswordCallback: (password: string) => Promise, ) { @@ -405,13 +409,20 @@ export class OverlayBackground implements OverlayBackgroundInterface { currentTab: chrome.tabs.Tab, updateAllCipherTypes: boolean, ): Promise { - if (updateAllCipherTypes || !this.cardAndIdentityCiphers) { - return this.getAllCipherTypeViews(currentTab); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (!activeUserId) { + return []; } - const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort( - (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), - ); + if (updateAllCipherTypes || !this.cardAndIdentityCiphers) { + return this.getAllCipherTypeViews(currentTab, activeUserId); + } + + const cipherViews = ( + await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", activeUserId) + ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); return this.cardAndIdentityCiphers ? cipherViews.concat(...this.cardAndIdentityCiphers) @@ -422,15 +433,19 @@ export class OverlayBackground implements OverlayBackgroundInterface { * Queries all cipher types from the user's vault returns them sorted by last used. * * @param currentTab - The current tab + * @param userId - The active user id */ - private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise { + private async getAllCipherTypeViews( + currentTab: chrome.tabs.Tab, + userId: UserId, + ): Promise { if (!this.cardAndIdentityCiphers) { this.cardAndIdentityCiphers = new Set([]); } this.cardAndIdentityCiphers.clear(); const cipherViews = ( - await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [ + await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", userId, [ CipherType.Card, CipherType.Identity, ]) @@ -2399,10 +2414,14 @@ export class OverlayBackground implements OverlayBackgroundInterface { try { this.closeInlineMenu(sender); - await this.cipherService.setAddEditCipherInfo({ - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.setAddEditCipherInfo( + { + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }, + activeUserId, + ); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id, diff --git a/apps/browser/src/autofill/background/web-request.background.ts b/apps/browser/src/autofill/background/web-request.background.ts index 2c14358a359..22e10a3dd0a 100644 --- a/apps/browser/src/autofill/background/web-request.background.ts +++ b/apps/browser/src/autofill/background/web-request.background.ts @@ -1,7 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -14,6 +18,7 @@ export default class WebRequestBackground { platformUtilsService: PlatformUtilsService, private cipherService: CipherService, private authService: AuthService, + private accountService: AccountService, private readonly webRequest: typeof chrome.webRequest, ) { this.isFirefox = platformUtilsService.isFirefox(); @@ -55,7 +60,16 @@ export default class WebRequestBackground { // eslint-disable-next-line private async resolveAuthCredentials(domain: string, success: Function, error: Function) { - if ((await this.authService.getAuthStatus()) < AuthenticationStatus.Unlocked) { + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + error(); + return; + } + + const authStatus = await firstValueFrom(this.authService.authStatusFor$(activeUserId)); + if (authStatus < AuthenticationStatus.Unlocked) { error(); return; } @@ -63,6 +77,7 @@ export default class WebRequestBackground { try { const ciphers = await this.cipherService.getAllDecryptedForUrl( domain, + activeUserId, null, UriMatchStrategy.Host, ); diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts index 4fed9eee5ef..3228aed4688 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.spec.ts @@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; @@ -14,6 +16,9 @@ describe("CipherContextMenuHandler", () => { let authService: MockProxy; let cipherService: MockProxy; + const mockUserId = "UserId" as UserId; + const accountService = mockAccountServiceWith(mockUserId); + let sut: CipherContextMenuHandler; beforeEach(() => { @@ -24,7 +29,12 @@ describe("CipherContextMenuHandler", () => { jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue(); - sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService); + sut = new CipherContextMenuHandler( + mainContextMenuHandler, + authService, + cipherService, + accountService, + ); }); afterEach(() => jest.resetAllMocks()); @@ -119,10 +129,11 @@ describe("CipherContextMenuHandler", () => { expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", [ - CipherType.Card, - CipherType.Identity, - ]); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith( + "https://test.com", + mockUserId, + [CipherType.Card, CipherType.Identity], + ); expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3); diff --git a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts index b112ff00efe..038f4e85c9a 100644 --- a/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts +++ b/apps/browser/src/autofill/browser/cipher-context-menu-handler.ts @@ -1,5 +1,9 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -14,6 +18,7 @@ export class CipherContextMenuHandler { private mainContextMenuHandler: MainContextMenuHandler, private authService: AuthService, private cipherService: CipherService, + private accountService: AccountService, ) {} async update(url: string) { @@ -35,7 +40,14 @@ export class CipherContextMenuHandler { return; } - const ciphers = await this.cipherService.getAllDecryptedForUrl(url, [ + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(url, activeUserId, [ CipherType.Card, CipherType.Identity, ]); diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts index 6ef004f7979..c8cb7e81f72 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.spec.ts @@ -61,6 +61,8 @@ describe("ContextMenuClickedHandler", () => { return cipherView; }; + const mockUserId = "UserId" as UserId; + let copyToClipboard: CopyToClipboardAction; let generatePasswordToClipboard: GeneratePasswordToClipboardAction; let autofill: AutofillAction; @@ -79,7 +81,7 @@ describe("ContextMenuClickedHandler", () => { autofill = jest.fn, [tab: chrome.tabs.Tab, cipher: CipherView]>(); authService = mock(); cipherService = mock(); - accountService = mockAccountServiceWith("userId" as UserId); + accountService = mockAccountServiceWith(mockUserId as UserId); totpService = mock(); eventCollectionService = mock(); @@ -191,7 +193,11 @@ describe("ContextMenuClickedHandler", () => { expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith( + "https://test.com", + mockUserId, + [], + ); expect(copyToClipboard).toHaveBeenCalledTimes(1); @@ -215,7 +221,11 @@ describe("ContextMenuClickedHandler", () => { expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith( + "https://test.com", + mockUserId, + [], + ); }); }); }); diff --git a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts index 597d75575b0..69c8b6e70b8 100644 --- a/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts +++ b/apps/browser/src/autofill/browser/context-menu-clicked-handler.ts @@ -1,12 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { AUTOFILL_CARD_ID, AUTOFILL_ID, @@ -105,6 +106,13 @@ export class ContextMenuClickedHandler { menuItemId as string, ); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return; + } + if (isCreateCipherAction) { // pass; defer to logic below } else if (menuItemId === NOOP_COMMAND_SUFFIX) { @@ -120,12 +128,13 @@ export class ContextMenuClickedHandler { // in scenarios like unlock on autofill const ciphers = await this.cipherService.getAllDecryptedForUrl( tab.url, + activeUserId, additionalCiphersToGet, ); cipher = ciphers[0]; } else { - const ciphers = await this.cipherService.getAllDecrypted(); + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); cipher = ciphers.find(({ id }) => id === menuItemId); } @@ -133,9 +142,6 @@ export class ContextMenuClickedHandler { return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); await this.accountService.setAccountActivity(activeUserId, new Date()); switch (info.parentMenuItemId) { case AUTOFILL_ID: diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts index 2c22097f3d0..497664542ad 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.spec.ts @@ -110,6 +110,7 @@ describe("OverlayBackground", () => { i18nService, platformUtilsService, themeStateService, + accountService, ); jest @@ -205,7 +206,7 @@ describe("OverlayBackground", () => { await overlayBackground.updateOverlayCiphers(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); - expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); + expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId); expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( new Map([ diff --git a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts index 5dfade0f863..d0fad4cd00e 100644 --- a/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts +++ b/apps/browser/src/autofill/deprecated/background/overlay.background.deprecated.ts @@ -1,7 +1,8 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom } from "rxjs"; +import { firstValueFrom, map } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; @@ -106,6 +107,7 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { private i18nService: I18nService, private platformUtilsService: PlatformUtilsService, private themeStateService: ThemeStateService, + private accountService: AccountService, ) {} /** @@ -152,9 +154,13 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { } this.overlayLoginCiphers = new Map(); - const ciphersViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url)).sort( - (a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const ciphersViews = ( + await this.cipherService.getAllDecryptedForUrl(currentTab.url, activeUserId) + ).sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) { this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); } @@ -660,10 +666,16 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface { cipherView.type = CipherType.Login; cipherView.login = loginView; - await this.cipherService.setAddEditCipherInfo({ - cipher: cipherView, - collectionIds: cipherView.collectionIds, - }); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + await this.cipherService.setAddEditCipherInfo( + { + cipher: cipherView, + collectionIds: cipherView.collectionIds, + }, + activeUserId, + ); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); diff --git a/apps/browser/src/autofill/popup/fido2/fido2.component.ts b/apps/browser/src/autofill/popup/fido2/fido2.component.ts index 4555d87f249..24c14c98685 100644 --- a/apps/browser/src/autofill/popup/fido2/fido2.component.ts +++ b/apps/browser/src/autofill/popup/fido2/fido2.component.ts @@ -20,6 +20,7 @@ import { import { JslibModule } from "@bitwarden/angular/jslib.module"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -186,7 +187,10 @@ export class Fido2Component implements OnInit, OnDestroy { this.domainSettingsService.getUrlEquivalentDomains(this.url), ); - this.ciphers = (await this.cipherService.getAllDecrypted()).filter( + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + this.ciphers = (await this.cipherService.getAllDecrypted(activeUserId)).filter( (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted, ); @@ -211,7 +215,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.cipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); + const cipher = await this.cipherService.get(cipherId, activeUserId); return cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -232,7 +236,7 @@ export class Fido2Component implements OnInit, OnDestroy { this.ciphers = await Promise.all( message.existingCipherIds.map(async (cipherId) => { - const cipher = await this.cipherService.get(cipherId); + const cipher = await this.cipherService.get(cipherId, activeUserId); return cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); diff --git a/apps/browser/src/autofill/services/autofill.service.spec.ts b/apps/browser/src/autofill/services/autofill.service.spec.ts index 16b11b98866..3843734ad64 100644 --- a/apps/browser/src/autofill/services/autofill.service.spec.ts +++ b/apps/browser/src/autofill/services/autofill.service.spec.ts @@ -769,7 +769,10 @@ describe("AutofillService", () => { ); expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled(); expect(logService.info).not.toHaveBeenCalled(); - expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith(autofillOptions.cipher.id); + expect(cipherService.updateLastUsedDate).toHaveBeenCalledWith( + autofillOptions.cipher.id, + mockUserId, + ); expect(chrome.tabs.sendMessage).toHaveBeenCalledWith( autofillOptions.pageDetails[0].tab.id, { @@ -1030,8 +1033,8 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, false); expect(cipherService.getNextCipherForUrl).not.toHaveBeenCalled(); - expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true); - expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true); + expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true); + expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true); expect(autofillService.doAutoFill).not.toHaveBeenCalled(); expect(result).toBeNull(); }); @@ -1044,7 +1047,7 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); - expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url); + expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId); expect(cipherService.getLastLaunchedForUrl).not.toHaveBeenCalled(); expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).not.toHaveBeenCalled(); @@ -1074,7 +1077,7 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); - expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true); + expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true); expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled(); expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).toHaveBeenCalledWith({ @@ -1104,8 +1107,8 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); - expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true); - expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true); + expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true); + expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true); expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).toHaveBeenCalledWith({ tab: tab, @@ -1132,7 +1135,7 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); - expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url); + expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId); expect(cipherService.updateLastUsedIndexForUrl).toHaveBeenCalledWith(tab.url); expect(autofillService.doAutoFill).toHaveBeenCalledWith({ tab: tab, @@ -1163,7 +1166,7 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); - expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url); + expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId); expect(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled(); expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, { cipherId: cipher.id, @@ -1189,7 +1192,7 @@ describe("AutofillService", () => { const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); - expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url); + expect(cipherService.getNextCipherForUrl).toHaveBeenCalledWith(tab.url, mockUserId); expect(autofillService["openVaultItemPasswordRepromptPopout"]).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).not.toHaveBeenCalled(); expect(result).toBeNull(); diff --git a/apps/browser/src/autofill/services/autofill.service.ts b/apps/browser/src/autofill/services/autofill.service.ts index 6d0e9954ade..fc7d0ebcc99 100644 --- a/apps/browser/src/autofill/services/autofill.service.ts +++ b/apps/browser/src/autofill/services/autofill.service.ts @@ -8,6 +8,7 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { AutofillOverlayVisibility, CardExpiryDateDelimiters, @@ -464,7 +465,7 @@ export default class AutofillService implements AutofillServiceInterface { didAutofill = true; if (!options.skipLastUsed) { - await this.cipherService.updateLastUsedDate(options.cipher.id); + await this.cipherService.updateLastUsedDate(options.cipher.id, activeAccount.id); } // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. @@ -527,17 +528,29 @@ export default class AutofillService implements AutofillServiceInterface { autoSubmitLogin = false, ): Promise { let cipher: CipherView; + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return null; + } + if (fromCommand) { - cipher = await this.cipherService.getNextCipherForUrl(tab.url); + cipher = await this.cipherService.getNextCipherForUrl(tab.url, activeUserId); } else { - const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(tab.url, true); + const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl( + tab.url, + activeUserId, + true, + ); if ( lastLaunchedCipher && Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000 ) { cipher = lastLaunchedCipher; } else { - cipher = await this.cipherService.getLastUsedForUrl(tab.url, true); + cipher = await this.cipherService.getLastUsedForUrl(tab.url, activeUserId, true); } } @@ -626,12 +639,19 @@ export default class AutofillService implements AutofillServiceInterface { let cipher: CipherView; let cacheKey = ""; + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (activeUserId == null) { + return null; + } + if (cipherType === CipherType.Card) { cacheKey = "cardCiphers"; - cipher = await this.cipherService.getNextCardCipher(); + cipher = await this.cipherService.getNextCardCipher(activeUserId); } else { cacheKey = "identityCiphers"; - cipher = await this.cipherService.getNextIdentityCipher(); + cipher = await this.cipherService.getNextIdentityCipher(activeUserId); } if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) { diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 68fa65f8528..d2b51c7ef40 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1258,6 +1258,7 @@ export default class MainBackground { this.mainContextMenuHandler, this.authService, this.cipherService, + this.accountService, ); if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { @@ -1265,6 +1266,7 @@ export default class MainBackground { this.platformUtilsService, this.cipherService, this.authService, + this.accountService, chrome.webRequest, ); } @@ -1636,6 +1638,7 @@ export default class MainBackground { this.i18nService, this.platformUtilsService, this.themeStateService, + this.accountService, ); } else { this.overlayBackground = new OverlayBackground( @@ -1653,6 +1656,7 @@ export default class MainBackground { this.inlineMenuFieldQualificationService, this.themeStateService, this.totpService, + this.accountService, () => this.generatePassword(), (password) => this.addPasswordToHistory(password), ); diff --git a/apps/browser/src/platform/listeners/update-badge.ts b/apps/browser/src/platform/listeners/update-badge.ts index 9f2a1f5a057..cd74b9ebf7d 100644 --- a/apps/browser/src/platform/listeners/update-badge.ts +++ b/apps/browser/src/platform/listeners/update-badge.ts @@ -2,8 +2,10 @@ // @ts-strict-ignore import { firstValueFrom } from "rxjs"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -22,6 +24,7 @@ export class UpdateBadge { private authService: AuthService; private badgeSettingsService: BadgeSettingsServiceAbstraction; private cipherService: CipherService; + private accountService: AccountService; private badgeAction: typeof chrome.action | typeof chrome.browserAction; private sidebarAction: OperaSidebarAction | FirefoxSidebarAction; private win: Window & typeof globalThis; @@ -34,6 +37,7 @@ export class UpdateBadge { this.badgeSettingsService = services.badgeSettingsService; this.authService = services.authService; this.cipherService = services.cipherService; + this.accountService = services.accountService; } async run(opts?: { tabId?: number; windowId?: number }): Promise { @@ -87,7 +91,14 @@ export class UpdateBadge { return; } - const ciphers = await this.cipherService.getAllDecryptedForUrl(opts?.tab?.url); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (!activeUserId) { + return; + } + + const ciphers = await this.cipherService.getAllDecryptedForUrl(opts?.tab?.url, activeUserId); let countText = ciphers.length == 0 ? "" : ciphers.length.toString(); if (ciphers.length > 9) { countText = "9+"; diff --git a/apps/browser/src/popup/app.component.ts b/apps/browser/src/popup/app.component.ts index 8147524d2f3..a97d001c54b 100644 --- a/apps/browser/src/popup/app.component.ts +++ b/apps/browser/src/popup/app.component.ts @@ -179,7 +179,7 @@ export class AppComponent implements OnInit, OnDestroy { await this.clearComponentStates(); } if (url.startsWith("/tabs/")) { - await this.cipherService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null, this.activeUserId); } (window as any).previousPopupUrl = url; diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts index 3252f030fc3..6974e6f7359 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.spec.ts @@ -1,14 +1,17 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute, Router } from "@angular/router"; import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, Observable } from "rxjs"; +import { BehaviorSubject } from "rxjs"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { EventType } from "@bitwarden/common/enums"; 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"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; @@ -58,9 +61,9 @@ describe("AddEditV2Component", () => { collect.mockClear(); addEditCipherInfo$ = new BehaviorSubject(null); - cipherServiceMock = mock(); - cipherServiceMock.addEditCipherInfo$ = - addEditCipherInfo$.asObservable() as Observable; + cipherServiceMock = mock({ + addEditCipherInfo$: jest.fn().mockReturnValue(addEditCipherInfo$), + }); await TestBed.configureTestingModule({ imports: [AddEditV2Component], @@ -81,6 +84,7 @@ describe("AddEditV2Component", () => { canDeleteCipher$: jest.fn().mockReturnValue(true), }, }, + { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, ], }) .overrideProvider(CipherFormConfigService, { diff --git a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts index b46b1d61509..1dcb48c918d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/add-edit/add-edit-v2.component.ts @@ -9,10 +9,12 @@ import { firstValueFrom, map, Observable, switchMap } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; -import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -180,6 +182,7 @@ export class AddEditV2Component implements OnInit { private toastService: ToastService, private dialogService: DialogService, protected cipherAuthorizationService: CipherAuthorizationService, + private accountService: AccountService, ) { this.subscribeToParams(); } @@ -281,9 +284,15 @@ export class AddEditV2Component implements OnInit { config.initialValues = this.setInitialValuesFromParams(params); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + // The browser notification bar and overlay use addEditCipherInfo$ to pass modified cipher details to the form // Attempt to fetch them here and overwrite the initialValues if present - const cachedCipherInfo = await firstValueFrom(this.cipherService.addEditCipherInfo$); + const cachedCipherInfo = await firstValueFrom( + this.cipherService.addEditCipherInfo$(activeUserId), + ); if (cachedCipherInfo != null) { // Cached cipher info has priority over queryParams @@ -292,7 +301,7 @@ export class AddEditV2Component implements OnInit { ...mapAddEditCipherInfoToInitialValues(cachedCipherInfo), }; // Be sure to clear the "cached" cipher info, so it doesn't get used again - await this.cipherService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null, activeUserId); } if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) { @@ -371,7 +380,8 @@ export class AddEditV2Component implements OnInit { } try { - await this.deleteCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipher(activeUserId); } catch (e) { this.logService.error(e); return false; @@ -388,10 +398,10 @@ export class AddEditV2Component implements OnInit { return true; }; - protected deleteCipher() { + protected deleteCipher(userId: UserId) { return this.config.originalCipher.deletedDate - ? this.cipherService.deleteWithServer(this.config.originalCipher.id) - : this.cipherService.softDeleteWithServer(this.config.originalCipher.id); + ? this.cipherService.deleteWithServer(this.config.originalCipher.id, userId) + : this.cipherService.softDeleteWithServer(this.config.originalCipher.id, userId); } } diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 51ebe9bdb62..27f3b7e5e18 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -5,12 +5,13 @@ import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { ReactiveFormsModule } from "@angular/forms"; import { ActivatedRoute } from "@angular/router"; -import { Observable, combineLatest, first, map, switchMap } from "rxjs"; +import { Observable, combineLatest, filter, first, map, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; +import { OrgKey, UserKey } from "@bitwarden/common/types/key"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -58,16 +59,19 @@ export class AssignCollections { private accountService: AccountService, route: ActivatedRoute, ) { - const cipher$: Observable = route.queryParams.pipe( - switchMap(({ cipherId }) => this.cipherService.get(cipherId)), - switchMap((cipherDomain) => - this.accountService.activeAccount$.pipe( - map((account) => account?.id), - switchMap((userId) => - this.cipherService - .getKeyForCipherKeyDecryption(cipherDomain, userId) - .then(cipherDomain.decrypt.bind(cipherDomain)), - ), + const cipher$: Observable = this.accountService.activeAccount$.pipe( + map((account) => account?.id), + filter((userId) => userId != null), + switchMap((userId) => + route.queryParams.pipe( + switchMap(async ({ cipherId }) => { + const cipherDomain = await this.cipherService.get(cipherId, userId); + const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption( + cipherDomain, + userId, + ); + return cipherDomain.decrypt(key); + }), ), ), ); diff --git a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts index aca494716b1..1bc7e22e6d5 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/attachments/open-attachments/open-attachments.component.ts @@ -77,10 +77,10 @@ export class OpenAttachmentsComponent implements OnInit { return; } - const cipherDomain = await this.cipherService.get(this.cipherId); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); const cipher = await cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), ); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts index 1f67dd51c21..fb5ac4b3391 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-header/vault-header-v2.component.spec.ts @@ -59,7 +59,9 @@ describe("VaultHeaderV2Component", () => { providers: [ { provide: CipherService, - useValue: mock({ cipherViews$: new BehaviorSubject([]) }), + useValue: mock({ + cipherViews$: jest.fn().mockReturnValue(new BehaviorSubject([])), + }), }, { provide: VaultSettingsService, useValue: mock() }, { provide: FolderService, useValue: mock() }, diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts index f95790cda5f..cb758e7a48d 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-list-items-container/vault-list-items-container.component.ts @@ -22,6 +22,8 @@ import { Router } from "@angular/router"; import { firstValueFrom, Observable, map } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherId } from "@bitwarden/common/types/guid"; @@ -265,6 +267,7 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { private router: Router, private platformUtilsService: PlatformUtilsService, private dialogService: DialogService, + private accountService: AccountService, ) {} ngOnInit(): void { @@ -311,7 +314,8 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit { this.viewCipherTimeout = null; } - await this.cipherService.updateLastLaunchedDate(cipher.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.updateLastLaunchedDate(cipher.id, activeUserId); await BrowserApi.createNewTab(cipher.login.launchUri); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts index 9ac17b49386..838ce2e9426 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.spec.ts @@ -1,13 +1,15 @@ import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ActivatedRoute } from "@angular/router"; import { mock } from "jest-mock-extended"; -import { BehaviorSubject, Subject } from "rxjs"; +import { Subject } from "rxjs"; import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; -import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -19,6 +21,7 @@ import { PasswordHistoryV2Component } from "./vault-password-history-v2.componen describe("PasswordHistoryV2Component", () => { let fixture: ComponentFixture; const params$ = new Subject(); + const mockUserId = "acct-1" as UserId; const mockCipherView = { id: "111-222-333", @@ -45,9 +48,7 @@ describe("PasswordHistoryV2Component", () => { { provide: CipherService, useValue: mock({ get: getCipher }) }, { provide: AccountService, - useValue: mock({ - activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account), - }), + useValue: mockAccountServiceWith(mockUserId), }, { provide: PopupRouterCacheService, useValue: { back } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } }, @@ -64,7 +65,7 @@ describe("PasswordHistoryV2Component", () => { tick(100); - expect(getCipher).toHaveBeenCalledWith(mockCipherView.id); + expect(getCipher).toHaveBeenCalledWith(mockCipherView.id, mockUserId); })); it("navigates back when a cipherId is not in the params", () => { diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts index e20bb1f1bcd..5d315775b10 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-password-history-v2/vault-password-history-v2.component.ts @@ -58,8 +58,6 @@ export class PasswordHistoryV2Component implements OnInit { /** Load the cipher based on the given Id */ private async loadCipher(cipherId: string) { - const cipher = await this.cipherService.get(cipherId); - const activeAccount = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), ); @@ -69,6 +67,8 @@ export class PasswordHistoryV2Component implements OnInit { } const activeUserId = activeAccount.id as UserId; + + const cipher = await this.cipherService.get(cipherId, activeUserId); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); diff --git a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts index 12952a69c79..635ae82fc37 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/vault-v2.component.ts @@ -3,9 +3,20 @@ import { CommonModule } from "@angular/common"; import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { RouterLink } from "@angular/router"; -import { combineLatest, filter, map, Observable, shareReplay, switchMap, take } from "rxjs"; +import { + combineLatest, + filter, + map, + firstValueFrom, + Observable, + shareReplay, + switchMap, + take, +} from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -96,6 +107,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupListFiltersService: VaultPopupListFiltersService, private vaultScrollPositionService: VaultPopupScrollPositionService, + private accountService: AccountService, private destroyRef: DestroyRef, private cipherService: CipherService, private dialogService: DialogService, @@ -136,7 +148,10 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy { } async ngOnInit() { - this.cipherService.failedToDecryptCiphers$ + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + this.cipherService + .failedToDecryptCiphers$(activeUserId) .pipe( map((ciphers) => ciphers.filter((c) => !c.isDeleted)), filter((ciphers) => ciphers.length > 0), diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts index 39feb86f4fd..17464c025af 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.spec.ts @@ -150,7 +150,7 @@ describe("ViewV2Component", () => { flush(); // Resolve all promises - expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444"); + expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId); expect(component.cipher).toEqual(mockCipher); })); diff --git a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts index 65fb024ee99..209691869f0 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/view-v2/view-v2.component.ts @@ -5,13 +5,14 @@ import { Component } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormsModule } from "@angular/forms"; import { ActivatedRoute, Router } from "@angular/router"; -import { firstValueFrom, map, Observable, switchMap } from "rxjs"; +import { firstValueFrom, Observable, switchMap } from "rxjs"; import { CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { AUTOFILL_ID, COPY_PASSWORD_ID, @@ -22,6 +23,7 @@ import { import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; @@ -87,6 +89,8 @@ type LoadAction = ], }) export class ViewV2Component { + private activeUserId: UserId; + headerText: string; cipher: CipherView; organization$: Observable; @@ -117,14 +121,20 @@ export class ViewV2Component { subscribeToParams(): void { this.route.queryParams .pipe( - switchMap(async (params): Promise => { + switchMap(async (params) => { this.loadAction = params.action; this.senderTabId = params.senderTabId ? parseInt(params.senderTabId, 10) : undefined; - return await this.getCipherData(params.cipherId); + + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getUserId), + ); + const cipher = await this.getCipherData(params.cipherId, activeUserId); + return { activeUserId, cipher }; }), - switchMap(async (cipher) => { + switchMap(async ({ activeUserId, cipher }) => { this.cipher = cipher; this.headerText = this.setHeader(cipher.type); + this.activeUserId = activeUserId; if (this.loadAction) { await this._handleLoadAction(this.loadAction, this.senderTabId); @@ -159,13 +169,10 @@ export class ViewV2Component { } } - async getCipherData(id: string) { - const cipher = await this.cipherService.get(id); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + async getCipherData(id: string, userId: UserId) { + const cipher = await this.cipherService.get(id, userId); return await cipher.decrypt( - await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), + await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId), ); } @@ -213,7 +220,7 @@ export class ViewV2Component { restore = async (): Promise => { try { - await this.cipherService.restoreWithServer(this.cipher.id); + await this.cipherService.restoreWithServer(this.cipher.id, this.activeUserId); } catch (e) { this.logService.error(e); } @@ -228,8 +235,8 @@ export class ViewV2Component { protected deleteCipher() { return this.cipher.isDeleted - ? this.cipherService.deleteWithServer(this.cipher.id) - : this.cipherService.softDeleteWithServer(this.cipher.id); + ? this.cipherService.deleteWithServer(this.cipher.id, this.activeUserId) + : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId); } protected showFooter(): boolean { diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index ec20458ca60..6d7b7b57d23 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -15,6 +15,8 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; +import { LocalData } from "@bitwarden/common/vault/models/data/local.data"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; @@ -34,6 +36,10 @@ describe("VaultPopupItemsService", () => { let mockCollections: CollectionView[]; let activeUserLastSync$: BehaviorSubject; + let ciphersSubject: BehaviorSubject>; + let localDataSubject: BehaviorSubject>; + let failedToDecryptCiphersSubject: BehaviorSubject; + const cipherServiceMock = mock(); const vaultSettingsServiceMock = mock(); const organizationServiceMock = mock(); @@ -60,9 +66,21 @@ describe("VaultPopupItemsService", () => { cipherList[3].favorite = true; cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); - cipherServiceMock.ciphers$ = new BehaviorSubject(null); - cipherServiceMock.localData$ = new BehaviorSubject(null); - cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]); + + ciphersSubject = new BehaviorSubject>({}); + localDataSubject = new BehaviorSubject>({}); + failedToDecryptCiphersSubject = new BehaviorSubject([]); + + cipherServiceMock.ciphers$.mockImplementation((userId: UserId) => + ciphersSubject.asObservable(), + ); + cipherServiceMock.localData$.mockImplementation((userId: UserId) => + localDataSubject.asObservable(), + ); + cipherServiceMock.failedToDecryptCiphers$.mockImplementation((userId: UserId) => + failedToDecryptCiphersSubject.asObservable(), + ); + searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers); cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => ciphers.filter((c) => ["0", "1"].includes(c.id)), @@ -118,6 +136,7 @@ describe("VaultPopupItemsService", () => { { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, { provide: SyncService, useValue: syncServiceMock }, + { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, { provide: InlineMenuFieldQualificationService, useValue: inlineMenuFieldQualificationServiceMock, @@ -155,7 +174,7 @@ describe("VaultPopupItemsService", () => { await tracker.expectEmission(); - (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + ciphersSubject.next({}); await tracker.expectEmission(); @@ -169,7 +188,7 @@ describe("VaultPopupItemsService", () => { await tracker.expectEmission(); - (cipherServiceMock.localData$ as BehaviorSubject).next(null); + localDataSubject.next({}); await tracker.expectEmission(); @@ -373,7 +392,7 @@ describe("VaultPopupItemsService", () => { cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers); - (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + ciphersSubject.next({}); const deletedCiphers = await firstValueFrom(service.deletedCiphers$); expect(deletedCiphers.length).toBe(1); @@ -422,7 +441,7 @@ describe("VaultPopupItemsService", () => { it("should cycle when cipherService.ciphers$ emits", async () => { // Restart tracking tracked = new ObservableTracker(service.loading$); - (cipherServiceMock.ciphers$ as BehaviorSubject).next(null); + ciphersSubject.next({}); await trackedCiphers.pauseUntilReceived(2); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 8e0711abb1e..0b3e7eba492 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -1,6 +1,6 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { inject, Injectable, NgZone } from "@angular/core"; +import { Injectable, NgZone } from "@angular/core"; import { BehaviorSubject, combineLatest, @@ -86,16 +86,19 @@ export class VaultPopupItemsService { * Observable that contains the list of all decrypted ciphers. * @private */ - private _allDecryptedCiphers$: Observable = merge( - this.cipherService.ciphers$, - this.cipherService.localData$, - ).pipe( - runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular - tap(() => this._ciphersLoading$.next()), - waitUntilSync(this.syncService), - switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), - withLatestFrom(this.cipherService.failedToDecryptCiphers$), - map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), + private _allDecryptedCiphers$: Observable = this.accountService.activeAccount$.pipe( + map((a) => a?.id), + filter((userId) => userId != null), + switchMap((userId) => + merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe( + runInsideAngular(this.ngZone), + tap(() => this._ciphersLoading$.next()), + waitUntilSync(this.syncService), + switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted(userId))), + withLatestFrom(this.cipherService.failedToDecryptCiphers$(userId)), + map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), + ), + ), shareReplay({ refCount: true, bufferSize: 1 }), ); @@ -281,6 +284,7 @@ export class VaultPopupItemsService { private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, private accountService: AccountService, + private ngZone: NgZone, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 7f570e8f5c9..ec823d5738f 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -41,7 +41,7 @@ describe("VaultPopupListFiltersService", () => { } as unknown as FolderService; const cipherService = { - cipherViews$, + cipherViews$: () => cipherViews$, } as unknown as CipherService; const organizationService = { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 579319c92ab..b1451318499 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -93,16 +93,6 @@ export class VaultPopupListFiltersService { */ private cipherViews: CipherView[] = []; - /** - * Observable of cipher views - */ - private cipherViews$: Observable = this.cipherService.cipherViews$.pipe( - tap((cipherViews) => { - this.cipherViews = Object.values(cipherViews); - }), - map((ciphers) => Object.values(ciphers)), - ); - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); constructor( @@ -271,8 +261,16 @@ export class VaultPopupListFiltersService { * Folder array structured to be directly passed to `ChipSelectComponent` */ folders$: Observable[]> = this.activeUserId$.pipe( - switchMap((userId) => - combineLatest([ + switchMap((userId) => { + // Observable of cipher views + const cipherViews$ = this.cipherService.cipherViews$(userId).pipe( + tap((cipherViews) => { + this.cipherViews = Object.values(cipherViews); + }), + map((ciphers) => Object.values(ciphers)), + ); + + return combineLatest([ this.filters$.pipe( distinctUntilChanged( (previousFilter, currentFilter) => @@ -281,7 +279,7 @@ export class VaultPopupListFiltersService { ), ), this.folderService.folderViews$(userId), - this.cipherViews$, + cipherViews$, ]).pipe( map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { if (folders.length === 1 && folders[0].id === null) { @@ -330,8 +328,8 @@ export class VaultPopupListFiltersService { map((folders) => folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")), ), - ), - ), + ); + }), ); /** diff --git a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts index c56d1c7d10d..7e30ab26035 100644 --- a/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts +++ b/apps/browser/src/vault/popup/settings/trash-list-items-container/trash-list-items-container.component.ts @@ -3,8 +3,11 @@ import { CommonModule } from "@angular/common"; import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { Router } from "@angular/router"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId } from "@bitwarden/common/types/guid"; @@ -65,6 +68,7 @@ export class TrashListItemsContainerComponent { private i18nService: I18nService, private dialogService: DialogService, private passwordRepromptService: PasswordRepromptService, + private accountService: AccountService, private router: Router, ) {} @@ -81,7 +85,8 @@ export class TrashListItemsContainerComponent { async restore(cipher: CipherView) { try { - await this.cipherService.restoreWithServer(cipher.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.restoreWithServer(cipher.id, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ @@ -112,7 +117,8 @@ export class TrashListItemsContainerComponent { } try { - await this.cipherService.deleteWithServer(cipher.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.deleteWithServer(cipher.id, activeUserId); await this.router.navigate(["/trash"]); this.toastService.showToast({ diff --git a/apps/cli/src/admin-console/commands/share.command.ts b/apps/cli/src/admin-console/commands/share.command.ts index e26d073326e..6d9e6c8b6c0 100644 --- a/apps/cli/src/admin-console/commands/share.command.ts +++ b/apps/cli/src/admin-console/commands/share.command.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Response } from "../../models/response"; @@ -48,7 +49,9 @@ export class ShareCommand { organizationId = organizationId.toLowerCase(); } - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const cipher = await this.cipherService.get(id, activeUserId); if (cipher == null) { return Response.notFound(); } @@ -56,15 +59,12 @@ export class ShareCommand { return Response.badRequest("This item already belongs to an organization."); } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); const cipherView = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); try { await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId); - const updatedCipher = await this.cipherService.get(cipher.id); + const updatedCipher = await this.cipherService.get(cipher.id, activeUserId); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), ); diff --git a/apps/cli/src/commands/edit.command.ts b/apps/cli/src/commands/edit.command.ts index 9af28863c09..2d4a854135d 100644 --- a/apps/cli/src/commands/edit.command.ts +++ b/apps/cli/src/commands/edit.command.ts @@ -1,11 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { CollectionRequest } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CollectionExport } from "@bitwarden/common/models/export/collection.export"; @@ -25,8 +26,6 @@ import { CipherResponse } from "../vault/models/cipher.response"; import { FolderResponse } from "../vault/models/folder.response"; export class EditCommand { - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( private cipherService: CipherService, private folderService: FolderService, @@ -85,14 +84,12 @@ export class EditCommand { } private async editCipher(id: string, req: CipherExport) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.cipherService.get(id, activeUserId); if (cipher == null) { return Response.notFound(); } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); let cipherView = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -114,7 +111,9 @@ export class EditCommand { } private async editCipherCollections(id: string, req: string[]) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const cipher = await this.cipherService.get(id, activeUserId); if (cipher == null) { return Response.notFound(); } @@ -129,11 +128,14 @@ export class EditCommand { cipher.collectionIds = req; try { - const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); + const updatedCipher = await this.cipherService.saveCollectionsWithServer( + cipher, + activeUserId, + ); const decCipher = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption( updatedCipher, - await firstValueFrom(this.activeUserId$), + await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), ), ); const res = new CipherResponse(decCipher); @@ -144,7 +146,7 @@ export class EditCommand { } private async editFolder(id: string, req: FolderExport) { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const folder = await this.folderService.getFromState(id, activeUserId); if (folder == null) { return Response.notFound(); diff --git a/apps/cli/src/commands/get.command.ts b/apps/cli/src/commands/get.command.ts index a90bfa64894..92c3a8baeaf 100644 --- a/apps/cli/src/commands/get.command.ts +++ b/apps/cli/src/commands/get.command.ts @@ -52,8 +52,6 @@ import { FolderResponse } from "../vault/models/folder.response"; import { DownloadCommand } from "./download.command"; export class GetCommand extends DownloadCommand { - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( private cipherService: CipherService, private folderService: FolderService, @@ -114,16 +112,16 @@ export class GetCommand extends DownloadCommand { private async getCipherView(id: string): Promise { let decCipher: CipherView = null; + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (Utils.isGuid(id)) { - const cipher = await this.cipherService.get(id); + const cipher = await this.cipherService.get(id, activeUserId); if (cipher != null) { - const activeUserId = await firstValueFrom(this.activeUserId$); decCipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); } } else if (id.trim() !== "") { - let ciphers = await this.cipherService.getAllDecrypted(); + let ciphers = await this.cipherService.getAllDecrypted(activeUserId); ciphers = this.searchService.searchCiphersBasic(ciphers, id); if (ciphers.length > 1) { return ciphers; @@ -265,8 +263,10 @@ export class GetCommand extends DownloadCommand { const canAccessPremium = await firstValueFrom( this.accountProfileService.hasPremiumFromAnySource$(account.id), ); + if (!canAccessPremium) { - const originalCipher = await this.cipherService.get(cipher.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const originalCipher = await this.cipherService.get(cipher.id, activeUserId); if ( originalCipher == null || originalCipher.organizationId == null || @@ -352,7 +352,8 @@ export class GetCommand extends DownloadCommand { this.accountProfileService.hasPremiumFromAnySource$(account.id), ); if (!canAccessPremium) { - const originalCipher = await this.cipherService.get(cipher.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const originalCipher = await this.cipherService.get(cipher.id, activeUserId); if (originalCipher == null || originalCipher.organizationId == null) { return Response.error("Premium status is required to use this feature."); } @@ -384,7 +385,7 @@ export class GetCommand extends DownloadCommand { private async getFolder(id: string) { let decFolder: FolderView = null; - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (Utils.isGuid(id)) { const folder = await this.folderService.getFromState(id, activeUserId); if (folder != null) { @@ -561,7 +562,7 @@ export class GetCommand extends DownloadCommand { private async getFingerprint(id: string) { let fingerprint: string[] = null; if (id === "me") { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId)); fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey); } else if (Utils.isGuid(id)) { diff --git a/apps/cli/src/commands/list.command.ts b/apps/cli/src/commands/list.command.ts index 5e01af798a4..5d512d81bf5 100644 --- a/apps/cli/src/commands/list.command.ts +++ b/apps/cli/src/commands/list.command.ts @@ -65,11 +65,14 @@ export class ListCommand { private async listCiphers(options: Options) { let ciphers: CipherView[]; + + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + options.trash = options.trash || false; if (options.url != null && options.url.trim() !== "") { - ciphers = await this.cipherService.getAllDecryptedForUrl(options.url); + ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId); } else { - ciphers = await this.cipherService.getAllDecrypted(); + ciphers = await this.cipherService.getAllDecrypted(activeUserId); } if ( @@ -138,9 +141,8 @@ export class ListCommand { } private async listFolders(options: Options) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + let folders = await this.folderService.getAllDecryptedFromState(activeUserId); if (options.search != null && options.search.trim() !== "") { diff --git a/apps/cli/src/commands/restore.command.ts b/apps/cli/src/commands/restore.command.ts index 96179e7b687..fd2f6f239ef 100644 --- a/apps/cli/src/commands/restore.command.ts +++ b/apps/cli/src/commands/restore.command.ts @@ -1,9 +1,16 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Response } from "../models/response"; export class RestoreCommand { - constructor(private cipherService: CipherService) {} + constructor( + private cipherService: CipherService, + private accountService: AccountService, + ) {} async run(object: string, id: string): Promise { if (id != null) { @@ -19,7 +26,9 @@ export class RestoreCommand { } private async restoreCipher(id: string) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const cipher = await this.cipherService.get(id, activeUserId); if (cipher == null) { return Response.notFound(); } @@ -28,7 +37,7 @@ export class RestoreCommand { } try { - await this.cipherService.restoreWithServer(id); + await this.cipherService.restoreWithServer(id, activeUserId); return Response.success(); } catch (e) { return Response.error(e); diff --git a/apps/cli/src/oss-serve-configurator.ts b/apps/cli/src/oss-serve-configurator.ts index be476d19814..dec09447839 100644 --- a/apps/cli/src/oss-serve-configurator.ts +++ b/apps/cli/src/oss-serve-configurator.ts @@ -124,7 +124,10 @@ export class OssServeConfigurator { this.serviceContainer.encryptService, this.serviceContainer.organizationUserApiService, ); - this.restoreCommand = new RestoreCommand(this.serviceContainer.cipherService); + this.restoreCommand = new RestoreCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + ); this.shareCommand = new ShareCommand( this.serviceContainer.cipherService, this.serviceContainer.accountService, diff --git a/apps/cli/src/vault.program.ts b/apps/cli/src/vault.program.ts index f3eb6eef613..d6ef27b1428 100644 --- a/apps/cli/src/vault.program.ts +++ b/apps/cli/src/vault.program.ts @@ -347,7 +347,10 @@ export class VaultProgram extends BaseProgram { } await this.exitIfLocked(); - const command = new RestoreCommand(this.serviceContainer.cipherService); + const command = new RestoreCommand( + this.serviceContainer.cipherService, + this.serviceContainer.accountService, + ); const response = await command.run(object, id); this.processResponse(response); }); diff --git a/apps/cli/src/vault/create.command.ts b/apps/cli/src/vault/create.command.ts index 28f58187fdb..713471356c9 100644 --- a/apps/cli/src/vault/create.command.ts +++ b/apps/cli/src/vault/create.command.ts @@ -10,6 +10,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; @@ -30,8 +31,6 @@ import { CipherResponse } from "./models/cipher.response"; import { FolderResponse } from "./models/folder.response"; export class CreateCommand { - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( private cipherService: CipherService, private folderService: FolderService, @@ -90,7 +89,7 @@ export class CreateCommand { } private async createCipher(req: CipherExport) { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId); try { const newCipher = await this.cipherService.createWithServer(cipher); @@ -132,14 +131,14 @@ export class CreateCommand { return Response.badRequest("File name not provided."); } + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const itemId = options.itemId.toLowerCase(); - const cipher = await this.cipherService.get(itemId); + const cipher = await this.cipherService.get(itemId, activeUserId); if (cipher == null) { return Response.notFound(); } - const activeUserId = await firstValueFrom(this.activeUserId$); - const canAccessPremium = await firstValueFrom( this.accountProfileService.hasPremiumFromAnySource$(activeUserId), ); @@ -173,7 +172,7 @@ export class CreateCommand { } private async createFolder(req: FolderExport) { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); try { diff --git a/apps/cli/src/vault/delete.command.ts b/apps/cli/src/vault/delete.command.ts index a285f8f5b34..9e648cd9bb0 100644 --- a/apps/cli/src/vault/delete.command.ts +++ b/apps/cli/src/vault/delete.command.ts @@ -1,7 +1,8 @@ -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -44,7 +45,9 @@ export class DeleteCommand { } private async deleteCipher(id: string, options: Options) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const cipher = await this.cipherService.get(id, activeUserId); if (cipher == null) { return Response.notFound(); } @@ -59,9 +62,9 @@ export class DeleteCommand { try { if (options.permanent) { - await this.cipherService.deleteWithServer(id); + await this.cipherService.deleteWithServer(id, activeUserId); } else { - await this.cipherService.softDeleteWithServer(id); + await this.cipherService.softDeleteWithServer(id, activeUserId); } return Response.success(); } catch (e) { @@ -74,8 +77,10 @@ export class DeleteCommand { return Response.badRequest("`itemid` option is required."); } + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const itemId = options.itemId.toLowerCase(); - const cipher = await this.cipherService.get(itemId); + const cipher = await this.cipherService.get(itemId, activeUserId); if (cipher == null) { return Response.notFound(); } @@ -89,16 +94,19 @@ export class DeleteCommand { return Response.error("Attachment `" + id + "` was not found."); } - const account = await firstValueFrom(this.accountService.activeAccount$); const canAccessPremium = await firstValueFrom( - this.accountProfileService.hasPremiumFromAnySource$(account.id), + this.accountProfileService.hasPremiumFromAnySource$(activeUserId), ); if (cipher.organizationId == null && !canAccessPremium) { return Response.error("Premium status is required to use this feature."); } try { - await this.cipherService.deleteAttachmentWithServer(cipher.id, attachments[0].id); + await this.cipherService.deleteAttachmentWithServer( + cipher.id, + attachments[0].id, + activeUserId, + ); return Response.success(); } catch (e) { return Response.error(e); @@ -106,9 +114,7 @@ export class DeleteCommand { } private async deleteFolder(id: string) { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const folder = await this.folderService.getFromState(id, activeUserId); if (folder == null) { return Response.notFound(); diff --git a/apps/desktop/src/autofill/services/desktop-autofill.service.ts b/apps/desktop/src/autofill/services/desktop-autofill.service.ts index 1ce58596b34..2ed5a649997 100644 --- a/apps/desktop/src/autofill/services/desktop-autofill.service.ts +++ b/apps/desktop/src/autofill/services/desktop-autofill.service.ts @@ -4,6 +4,7 @@ import { EMPTY, Subject, distinctUntilChanged, + filter, firstValueFrom, map, mergeMap, @@ -12,6 +13,7 @@ import { } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; @@ -27,6 +29,7 @@ import { Utils } from "@bitwarden/common/platform/misc/utils"; import { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-utils"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -60,7 +63,11 @@ export class DesktopAutofillService implements OnDestroy { return EMPTY; } - return this.cipherService.cipherViews$; + return this.accountService.activeAccount$.pipe( + map((account) => account?.id), + filter((userId): userId is UserId => userId != null), + switchMap((userId) => this.cipherService.cipherViews$(userId)), + ); }), // TODO: This will unset all the autofill credentials on the OS // when the account locks. We should instead explicilty clear the credentials @@ -164,17 +171,22 @@ export class DesktopAutofillService implements OnDestroy { // TODO: For some reason the credentialId is passed as an empty array in the request, so we need to // get it from the cipher. For that we use the recordIdentifier, which is the cipherId. if (request.recordIdentifier && request.credentialId.length === 0) { - const cipher = await this.cipherService.get(request.recordIdentifier); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + if (!activeUserId) { + this.logService.error("listenPasskeyAssertion error", "Active user not found"); + callback(new Error("Active user not found"), null); + return; + } + + const cipher = await this.cipherService.get(request.recordIdentifier, activeUserId); if (!cipher) { this.logService.error("listenPasskeyAssertion error", "Cipher not found"); callback(new Error("Cipher not found"), null); return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const decrypted = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); diff --git a/apps/desktop/src/platform/services/ssh-agent.service.ts b/apps/desktop/src/platform/services/ssh-agent.service.ts index d4c7c5f460e..31b615dd365 100644 --- a/apps/desktop/src/platform/services/ssh-agent.service.ts +++ b/apps/desktop/src/platform/services/ssh-agent.service.ts @@ -29,6 +29,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { DialogService, ToastService } from "@bitwarden/components"; @@ -92,14 +93,14 @@ export class SshAgentService implements OnDestroy { }), filter(({ enabled }) => enabled), map(({ message }) => message), - withLatestFrom(this.authService.activeAccountStatus$), + withLatestFrom(this.authService.activeAccountStatus$, this.accountService.activeAccount$), // This switchMap handles unlocking the vault if it is locked: // - If the vault is locked, we will wait for it to be unlocked. // - If the vault is not unlocked within the timeout, we will abort the flow. // - If the vault is unlocked, we will continue with the flow. // switchMap is used here to prevent multiple requests from being processed at the same time, // and will cancel the previous request if a new one is received. - switchMap(([message, status]) => { + switchMap(([message, status, account]) => { if (status !== AuthenticationStatus.Unlocked) { ipc.platform.focusWindow(); this.toastService.showToast({ @@ -133,11 +134,11 @@ export class SshAgentService implements OnDestroy { ); } - return of(message); + return of([message, account.id]); }), // This switchMap handles fetching the ciphers from the vault. - switchMap((message) => - from(this.cipherService.getAllDecrypted()).pipe( + switchMap(([message, userId]: [Record, UserId]) => + from(this.cipherService.getAllDecrypted(userId)).pipe( map((ciphers) => [message, ciphers] as const), ), ), @@ -245,7 +246,7 @@ export class SshAgentService implements OnDestroy { return; } - const ciphers = await this.cipherService.getAllDecrypted(); + const ciphers = await this.cipherService.getAllDecrypted(activeAccount.id); if (ciphers == null) { await ipc.platform.sshAgent.lock(); return; diff --git a/apps/desktop/src/services/encrypted-message-handler.service.ts b/apps/desktop/src/services/encrypted-message-handler.service.ts index 43c4b9065a7..a8a1e738644 100644 --- a/apps/desktop/src/services/encrypted-message-handler.service.ts +++ b/apps/desktop/src/services/encrypted-message-handler.service.ts @@ -1,12 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service"; import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -65,9 +66,7 @@ export class EncryptedMessageHandlerService { } private async checkUserStatus(userId: string): Promise { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (userId !== activeUserId) { return "not-active-user"; @@ -83,9 +82,7 @@ export class EncryptedMessageHandlerService { private async statusCommandHandler(): Promise { const accounts = await firstValueFrom(this.accountService.accounts$); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); if (!accounts || !Object.keys(accounts)) { return []; @@ -114,16 +111,14 @@ export class EncryptedMessageHandlerService { } const ciphersResponse: CipherResponse[] = []; - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const authStatus = await this.authService.getAuthStatus(activeUserId); if (authStatus !== AuthenticationStatus.Unlocked) { return { error: "locked" }; } - const ciphers = await this.cipherService.getAllDecryptedForUrl(payload.uri); + const ciphers = await this.cipherService.getAllDecryptedForUrl(payload.uri, activeUserId); ciphers.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b)); ciphers.forEach((c) => { @@ -166,9 +161,7 @@ export class EncryptedMessageHandlerService { cipherView.login.uris[0].uri = credentialCreatePayload.uri; try { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const encrypted = await this.cipherService.encrypt(cipherView, activeUserId); await this.cipherService.createWithServer(encrypted); @@ -200,13 +193,16 @@ export class EncryptedMessageHandlerService { } try { - const cipher = await this.cipherService.get(credentialUpdatePayload.credentialId); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + const cipher = await this.cipherService.get( + credentialUpdatePayload.credentialId, + activeUserId, + ); if (cipher === null) { return { status: "failure" }; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const cipherView = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); diff --git a/apps/desktop/src/vault/app/vault/vault-items.component.ts b/apps/desktop/src/vault/app/vault/vault-items.component.ts index 348071729e8..5d7285e570b 100644 --- a/apps/desktop/src/vault/app/vault/vault-items.component.ts +++ b/apps/desktop/src/vault/app/vault/vault-items.component.ts @@ -5,6 +5,7 @@ import { distinctUntilChanged } from "rxjs"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -20,8 +21,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent { searchService: SearchService, searchBarService: SearchBarService, cipherService: CipherService, + accountService: AccountService, ) { - super(searchService, cipherService); + super(searchService, cipherService, accountService); // eslint-disable-next-line rxjs-angular/prefer-takeuntil searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => { diff --git a/apps/desktop/src/vault/app/vault/vault.component.ts b/apps/desktop/src/vault/app/vault/vault.component.ts index 8ee7cce3c77..aba7353c5e4 100644 --- a/apps/desktop/src/vault/app/vault/vault.component.ts +++ b/apps/desktop/src/vault/app/vault/vault.component.ts @@ -10,7 +10,7 @@ import { ViewContainerRef, } from "@angular/core"; import { ActivatedRoute, Router } from "@angular/router"; -import { combineLatest, firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; +import { firstValueFrom, Subject, takeUntil, switchMap } from "rxjs"; import { filter, first, map, take } from "rxjs/operators"; import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; @@ -19,6 +19,7 @@ import { VaultFilter } from "@bitwarden/angular/vault/vault-filter/models/vault- import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; @@ -236,15 +237,12 @@ export class VaultComponent implements OnInit, OnDestroy { }); } - // Store a reference to the current active account during page init - const activeAccount = await firstValueFrom(this.accountService.activeAccount$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - // Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit. - // The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions - combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$]) + this.cipherService + .failedToDecryptCiphers$(activeUserId) .pipe( - filter(([account]) => account.id === activeAccount.id), - map(([_, ciphers]) => ciphers.filter((c) => !c.isDeleted)), + map((ciphers) => ciphers.filter((c) => !c.isDeleted)), filter((ciphers) => ciphers.length > 0), take(1), takeUntil(this.componentIsDestroyed$), diff --git a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts index 2a972198cc5..5601c2f141f 100644 --- a/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/exposed-passwords-report.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service"; @@ -11,6 +11,7 @@ import { OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -59,15 +60,13 @@ export class ExposedPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.organization = await firstValueFrom( this.organizationService .organizations$(userId) .pipe(getOrganizationById(params.organizationId)), ); - this.manageableCiphers = await this.cipherService.getAll(); + this.manageableCiphers = await this.cipherService.getAll(userId); }); } diff --git a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts index 3609a9fe146..19f5d3607fa 100644 --- a/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/reused-passwords-report.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { @@ -10,6 +10,7 @@ import { OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; @@ -56,15 +57,13 @@ export class ReusedPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.organization = await firstValueFrom( this.organizationService .organizations$(userId) .pipe(getOrganizationById(params.organizationId)), ); - this.manageableCiphers = await this.cipherService.getAll(); + this.manageableCiphers = await this.cipherService.getAll(userId); await super.ngOnInit(); }); } diff --git a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts index 2bd6285d4f3..7c1c71b934d 100644 --- a/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts +++ b/apps/web/src/app/admin-console/organizations/tools/weak-passwords-report.component.ts @@ -2,7 +2,7 @@ // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; import { ActivatedRoute } from "@angular/router"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { @@ -10,6 +10,7 @@ import { OrganizationService, } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -59,15 +60,14 @@ export class WeakPasswordsReportComponent this.isAdminConsoleActive = true; // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe this.route.parent.parent.params.subscribe(async (params) => { - const userId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.organization = await firstValueFrom( this.organizationService .organizations$(userId) .pipe(getOrganizationById(params.organizationId)), ); - this.manageableCiphers = await this.cipherService.getAll(); + this.manageableCiphers = await this.cipherService.getAll(userId); await super.ngOnInit(); }); } diff --git a/apps/web/src/app/auth/settings/change-password.component.ts b/apps/web/src/app/auth/settings/change-password.component.ts index dd5c56f9b91..eb98f7fde07 100644 --- a/apps/web/src/app/auth/settings/change-password.component.ts +++ b/apps/web/src/app/auth/settings/change-password.component.ts @@ -88,7 +88,9 @@ export class ChangePasswordComponent async rotateUserKeyClicked() { if (this.rotateUserKey) { - const ciphers = await this.cipherService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); let hasOldAttachments = false; if (ciphers != null) { for (let i = 0; i < ciphers.length; i++) { diff --git a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts index f78b2920410..792563c4fab 100644 --- a/apps/web/src/app/tools/reports/pages/cipher-report.component.ts +++ b/apps/web/src/app/tools/reports/pages/cipher-report.component.ts @@ -1,12 +1,13 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; -import { BehaviorSubject, Observable, Subject, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, Observable, Subject, firstValueFrom, switchMap, takeUntil } from "rxjs"; import { ModalService } from "@bitwarden/angular/services/modal.service"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -51,8 +52,10 @@ export class CipherReportComponent implements OnDestroy { private syncService: SyncService, ) { this.organizations$ = this.accountService.activeAccount$.pipe( - switchMap((account) => this.organizationService.organizations$(account?.id)), + getUserId, + switchMap((userId) => this.organizationService.organizations$(userId)), ); + this.organizations$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => { this.organizations = orgs; }); @@ -182,7 +185,8 @@ export class CipherReportComponent implements OnDestroy { } protected async getAllCiphers(): Promise { - return await this.cipherService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + return await this.cipherService.getAllDecrypted(activeUserId); } protected filterCiphersByOrg(ciphersList: CipherView[]) { diff --git a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts index 16541bdc109..47e4a9d3652 100644 --- a/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/exposed-passwords-report.component.spec.ts @@ -25,15 +25,14 @@ describe("ExposedPasswordsReportComponent", () => { let auditService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; - let accountService: FakeAccountService; const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(() => { syncServiceMock = mock(); auditService = mock(); organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); - accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ diff --git a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts index 385bda03f28..033b88ea34f 100644 --- a/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/inactive-two-factor-report.component.spec.ts @@ -24,14 +24,13 @@ describe("InactiveTwoFactorReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; - let accountService: FakeAccountService; const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(() => { organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); - accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ diff --git a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts index 6a26cd24fe5..7c895423159 100644 --- a/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/reused-passwords-report.component.spec.ts @@ -23,15 +23,13 @@ describe("ReusedPasswordsReportComponent", () => { let fixture: ComponentFixture; let organizationService: MockProxy; let syncServiceMock: MockProxy; - let accountService: FakeAccountService; const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(() => { organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); - accountService = mockAccountServiceWith(userId); - // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ diff --git a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts index 7cd159108b8..337aa41b56b 100644 --- a/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/unsecured-websites-report.component.spec.ts @@ -25,15 +25,14 @@ describe("UnsecuredWebsitesReportComponent", () => { let organizationService: MockProxy; let syncServiceMock: MockProxy; let collectionService: MockProxy; - let accountService: FakeAccountService; const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(() => { organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); syncServiceMock = mock(); collectionService = mock(); - accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ diff --git a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts index 578c220f396..3d19511a252 100644 --- a/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts +++ b/apps/web/src/app/tools/reports/pages/weak-passwords-report.component.spec.ts @@ -25,15 +25,14 @@ describe("WeakPasswordsReportComponent", () => { let passwordStrengthService: MockProxy; let organizationService: MockProxy; let syncServiceMock: MockProxy; - let accountService: FakeAccountService; const userId = Utils.newGuid() as UserId; + const accountService: FakeAccountService = mockAccountServiceWith(userId); beforeEach(() => { syncServiceMock = mock(); passwordStrengthService = mock(); organizationService = mock(); organizationService.organizations$.mockReturnValue(of([])); - accountService = mockAccountServiceWith(userId); // FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling. // eslint-disable-next-line @typescript-eslint/no-floating-promises TestBed.configureTestingModule({ diff --git a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts index eb2289d7229..3dc3b8f9f84 100644 --- a/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts +++ b/apps/web/src/app/vault/components/vault-item-dialog/vault-item-dialog.component.ts @@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions"; import { EventType } from "@bitwarden/common/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -355,8 +356,8 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { this.formConfig.mode = "edit"; this.formConfig.initialValues = null; } - - let cipher = await this.cipherService.get(cipherView.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + let cipher = await this.cipherService.get(cipherView.id, activeUserId); // When the form config is used within the Admin Console, retrieve the cipher from the admin endpoint (if not found in local state) if (this.formConfig.isAdminConsole && (cipher == null || this.formConfig.admin)) { @@ -448,10 +449,13 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { result.action === AttachmentDialogResult.Removed || result.action === AttachmentDialogResult.Uploaded ) { - const updatedCipher = await this.cipherService.get(this.formConfig.originalCipher?.id); const activeUserId = await firstValueFrom( this.accountService.activeAccount$.pipe(map((a) => a?.id)), ); + const updatedCipher = await this.cipherService.get( + this.formConfig.originalCipher?.id, + activeUserId, + ); const updatedCipherView = await updatedCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), @@ -490,9 +494,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { if (config.originalCipher == null) { return; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); return await config.originalCipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId), ); @@ -574,10 +576,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy { // - The cipher is unassigned const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned; + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(map((a) => a?.id)), + ); + if (this.cipher.isDeleted) { - await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); + await this.cipherService.deleteWithServer(this.cipher.id, activeUserId, asAdmin); } else { - await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); + await this.cipherService.softDeleteWithServer(this.cipher.id, activeUserId, asAdmin); } } diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts index becfcb8f588..86019fe745e 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-delete-dialog/bulk-delete-dialog.component.ts @@ -2,10 +2,13 @@ // @ts-strict-ignore import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { Component, Inject } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -61,6 +64,7 @@ export class BulkDeleteDialogComponent { private apiService: ApiService, private collectionService: CollectionService, private toastService: ToastService, + private accountService: AccountService, ) { this.cipherIds = params.cipherIds ?? []; this.permanent = params.permanent; @@ -115,10 +119,12 @@ export class BulkDeleteDialogComponent { private async deleteCiphers(): Promise { const asAdmin = this.organization?.canEditAllCiphers; + + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (this.permanent) { - await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); + await this.cipherService.deleteManyWithServer(this.cipherIds, activeUserId, asAdmin); } else { - await this.cipherService.softDeleteManyWithServer(this.cipherIds, asAdmin); + await this.cipherService.softDeleteManyWithServer(this.cipherIds, activeUserId, asAdmin); } } diff --git a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts index 295d3ccc435..27e441e946e 100644 --- a/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts +++ b/apps/web/src/app/vault/individual-vault/bulk-action-dialogs/bulk-move-dialog/bulk-move-dialog.component.ts @@ -3,9 +3,10 @@ import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { Component, Inject, OnInit } from "@angular/core"; import { FormBuilder, Validators } from "@angular/forms"; -import { firstValueFrom, map, Observable } from "rxjs"; +import { firstValueFrom, Observable } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -48,8 +49,6 @@ export class BulkMoveDialogComponent implements OnInit { }); folders$: Observable; - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( @Inject(DIALOG_DATA) params: BulkMoveDialogParams, private dialogRef: DialogRef, @@ -65,7 +64,7 @@ export class BulkMoveDialogComponent implements OnInit { } async ngOnInit() { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); this.folders$ = this.folderService.folderViews$(activeUserId); this.formGroup.patchValue({ folderId: (await firstValueFrom(this.folders$))[0].id, @@ -81,7 +80,12 @@ export class BulkMoveDialogComponent implements OnInit { return; } - await this.cipherService.moveManyWithServer(this.cipherIds, this.formGroup.value.folderId); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.moveManyWithServer( + this.cipherIds, + this.formGroup.value.folderId, + activeUserId, + ); this.toastService.showToast({ variant: "success", title: null, diff --git a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts index 8d74f69ed06..15f5e1cd876 100644 --- a/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts +++ b/apps/web/src/app/vault/individual-vault/vault-filter/services/vault-filter.service.spec.ts @@ -71,7 +71,7 @@ describe("vault filter service", () => { policyService.policyAppliesToActiveUser$ .calledWith(PolicyType.SingleOrg) .mockReturnValue(singleOrgPolicy); - cipherService.cipherViews$ = cipherViews; + cipherService.cipherViews$.mockReturnValue(cipherViews); vaultFilterService = new VaultFilterService( organizationService, 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 03dfa92d0b5..df96edac140 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 @@ -68,7 +68,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction { switchMap((userId) => combineLatest([ this.folderService.folderViews$(userId), - this.cipherService.cipherViews$, + this.cipherService.cipherViews$(userId), this._organizationFilter, ]), ), 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 ff8a008bcc5..51c00ca18b8 100644 --- a/apps/web/src/app/vault/individual-vault/vault.component.ts +++ b/apps/web/src/app/vault/individual-vault/vault.component.ts @@ -44,6 +44,7 @@ import { } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; @@ -166,7 +167,6 @@ export class VaultComponent implements OnInit, OnDestroy { protected selectedCollection: TreeNode | undefined; protected canCreateCollections = false; protected currentSearchText$: Observable; - private activeUserId: UserId; private searchText$ = new Subject(); private refresh$ = new BehaviorSubject(null); private destroy$ = new Subject(); @@ -271,9 +271,7 @@ export class VaultComponent implements OnInit, OnDestroy { : "trashCleanupWarning", ); - this.activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const firstSetup$ = this.route.queryParams.pipe( first(), @@ -337,13 +335,15 @@ export class VaultComponent implements OnInit, OnDestroy { this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); const ciphers$ = combineLatest([ - this.cipherService.cipherViews$.pipe(filter((c) => c !== null)), + this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)), filter$, this.currentSearchText$, ]).pipe( filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), concatMap(async ([ciphers, filter, searchText]) => { - const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); + const failedCiphers = await firstValueFrom( + this.cipherService.failedToDecryptCiphers$(activeUserId), + ); const filterFunction = createFilterFunction(filter); // Append any failed to decrypt ciphers to the top of the cipher list const allCiphers = [...failedCiphers, ...ciphers]; @@ -416,7 +416,7 @@ export class VaultComponent implements OnInit, OnDestroy { switchMap(async (params) => { const cipherId = getCipherIdFromParams(params); if (cipherId) { - if (await this.cipherService.get(cipherId)) { + if (await this.cipherService.get(cipherId, activeUserId)) { let action = params.action; // Default to "view" if (action == null) { @@ -459,7 +459,7 @@ export class VaultComponent implements OnInit, OnDestroy { firstSetup$ .pipe( - switchMap(() => this.cipherService.failedToDecryptCiphers$), + switchMap(() => this.cipherService.failedToDecryptCiphers$(activeUserId)), map((ciphers) => ciphers.filter((c) => !c.isDeleted)), filter((ciphers) => ciphers.length > 0), take(1), @@ -480,7 +480,7 @@ export class VaultComponent implements OnInit, OnDestroy { switchMap(() => combineLatest([ filter$, - this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId), + this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId), allCollections$, this.organizations$, ciphers$, @@ -732,7 +732,8 @@ export class VaultComponent implements OnInit, OnDestroy { * @returns */ async editCipherId(id: string, cloneMode?: boolean) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const cipher = await this.cipherService.get(id, activeUserId); if ( cipher && @@ -768,7 +769,8 @@ export class VaultComponent implements OnInit, OnDestroy { * @returns Promise */ async viewCipherById(id: string) { - const cipher = await this.cipherService.get(id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.cipherService.get(id, activeUserId); // If cipher exists (cipher is null when new) and MP reprompt // is on for this cipher, then show password reprompt. if ( @@ -959,7 +961,8 @@ export class VaultComponent implements OnInit, OnDestroy { } try { - await this.cipherService.restoreWithServer(c.id); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.restoreWithServer(c.id, activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -1041,7 +1044,8 @@ export class VaultComponent implements OnInit, OnDestroy { } try { - await this.deleteCipherWithServer(c.id, permanent); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipherWithServer(c.id, activeUserId, permanent); this.toastService.showToast({ variant: "success", @@ -1176,10 +1180,10 @@ export class VaultComponent implements OnInit, OnDestroy { } } - protected deleteCipherWithServer(id: string, permanent: boolean) { + protected deleteCipherWithServer(id: string, userId: UserId, permanent: boolean) { return permanent - ? this.cipherService.deleteWithServer(id) - : this.cipherService.softDeleteWithServer(id); + ? this.cipherService.deleteWithServer(id, userId) + : this.cipherService.softDeleteWithServer(id, userId); } protected async repromptCipher(ciphers: CipherView[]) { diff --git a/apps/web/src/app/vault/individual-vault/view.component.ts b/apps/web/src/app/vault/individual-vault/view.component.ts index 9b6d15c581d..baae6f28bf1 100644 --- a/apps/web/src/app/vault/individual-vault/view.component.ts +++ b/apps/web/src/app/vault/individual-vault/view.component.ts @@ -165,10 +165,11 @@ export class ViewComponent implements OnInit { */ protected async deleteCipher(): Promise { const asAdmin = this.organization?.canEditAllCiphers; + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (this.cipher.isDeleted) { - await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); + await this.cipherService.deleteWithServer(this.cipher.id, userId, asAdmin); } else { - await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); + await this.cipherService.softDeleteWithServer(this.cipher.id, userId, asAdmin); } } diff --git a/apps/web/src/app/vault/org-vault/add-edit.component.ts b/apps/web/src/app/vault/org-vault/add-edit.component.ts index 75042a63e91..8490ec6c9db 100644 --- a/apps/web/src/app/vault/org-vault/add-edit.component.ts +++ b/apps/web/src/app/vault/org-vault/add-edit.component.ts @@ -2,6 +2,7 @@ // @ts-strict-ignore import { DatePipe } from "@angular/common"; import { Component } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; @@ -10,6 +11,7 @@ import { EventCollectionService } from "@bitwarden/common/abstractions/event/eve 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 { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -98,8 +100,9 @@ export class AddEditComponent extends BaseAddEditComponent { protected async loadCipher() { this.isAdminConsoleAction = true; + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); // Calling loadCipher first to assess if the cipher is unassigned. If null use apiService getCipherAdmin - const firstCipherCheck = await super.loadCipher(); + const firstCipherCheck = await super.loadCipher(activeUserId); if (!this.organization.canEditAllCiphers && firstCipherCheck != null) { return firstCipherCheck; @@ -123,7 +126,8 @@ export class AddEditComponent extends BaseAddEditComponent { protected async deleteCipher() { if (!this.organization.canEditAllCiphers) { - return super.deleteCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + return super.deleteCipher(activeUserId); } return this.cipher.isDeleted ? this.apiService.deleteCipherAdmin(this.cipherId) diff --git a/apps/web/src/app/vault/org-vault/attachments.component.ts b/apps/web/src/app/vault/org-vault/attachments.component.ts index 37136a86cdb..c2ad82bc27a 100644 --- a/apps/web/src/app/vault/org-vault/attachments.component.ts +++ b/apps/web/src/app/vault/org-vault/attachments.component.ts @@ -1,10 +1,12 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Component, OnInit } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; @@ -74,7 +76,8 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On protected async loadCipher() { if (!this.organization.canEditAllCiphers) { - return await super.loadCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + return await super.loadCipher(activeUserId); } const response = await this.apiService.getCipherAdmin(this.cipherId); return new Cipher(new CipherData(response)); @@ -89,9 +92,9 @@ export class AttachmentsComponent extends BaseAttachmentsComponent implements On ); } - protected deleteCipherAttachment(attachmentId: string) { + protected deleteCipherAttachment(attachmentId: string, userId: UserId) { if (!this.organization.canEditAllCiphers) { - return super.deleteCipherAttachment(attachmentId); + return super.deleteCipherAttachment(attachmentId, userId); } return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); } diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts index 5ccddeee4bb..9ee13bf077a 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.spec.ts @@ -7,7 +7,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; -import { Account, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -26,6 +27,7 @@ describe("AdminConsoleCipherFormConfigService", () => { isMember: true, enabled: true, status: OrganizationUserStatusType.Confirmed, + userId: "UserId", }; const testOrg2 = { id: "333-999-888", @@ -34,6 +36,7 @@ describe("AdminConsoleCipherFormConfigService", () => { isMember: true, enabled: true, status: OrganizationUserStatusType.Confirmed, + userId: "UserId", }; const policyAppliesToActiveUser$ = new BehaviorSubject(true); const collection = { @@ -80,17 +83,7 @@ describe("AdminConsoleCipherFormConfigService", () => { }, { provide: ApiService, useValue: { getCipherAdmin } }, { provide: CipherService, useValue: { get: getCipher } }, - { - provide: AccountService, - useValue: { - activeAccount$: new BehaviorSubject({ - id: "123-456-789" as UserId, - email: "test@email.com", - emailVerified: true, - name: "Test User", - }), - }, - }, + { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, ], }); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); @@ -207,7 +200,7 @@ describe("AdminConsoleCipherFormConfigService", () => { await adminConsoleConfigService.buildConfig("edit", cipherId); expect(getCipherAdmin).not.toHaveBeenCalled(); - expect(getCipher).toHaveBeenCalledWith(cipherId); + expect(getCipher).toHaveBeenCalledWith(cipherId, "UserId"); }); }); }); diff --git a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts index 32e75644d09..19259ba4033 100644 --- a/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts +++ b/apps/web/src/app/vault/org-vault/services/admin-console-cipher-form-config.service.ts @@ -10,7 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; @@ -100,7 +100,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ return null; } - const localCipher = await this.cipherService.get(id); + const localCipher = await this.cipherService.get(id, organization.userId as UserId); // Fetch from the API because we don't need the permissions in local state OR the cipher was not found (e.g. unassigned) if (organization.canEditAllCiphers || localCipher == null) { 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 b4ba9ff5512..f14a8fcf5ee 100644 --- a/apps/web/src/app/vault/org-vault/vault.component.ts +++ b/apps/web/src/app/vault/org-vault/vault.component.ts @@ -40,6 +40,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { EventType } from "@bitwarden/common/enums"; @@ -52,7 +53,7 @@ import { MessagingService } from "@bitwarden/common/platform/abstractions/messag import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -952,8 +953,9 @@ export class VaultComponent implements OnInit, OnDestroy { // Allow restore of an Unassigned Item try { + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned; - await this.cipherService.restoreWithServer(c.id, asAdmin); + await this.cipherService.restoreWithServer(c.id, activeUserId, asAdmin); this.toastService.showToast({ variant: "success", title: null, @@ -1044,7 +1046,8 @@ export class VaultComponent implements OnInit, OnDestroy { } try { - await this.deleteCipherWithServer(c.id, permanent, c.isUnassigned); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipherWithServer(c.id, activeUserId, permanent, c.isUnassigned); this.toastService.showToast({ variant: "success", title: null, @@ -1332,11 +1335,16 @@ export class VaultComponent implements OnInit, OnDestroy { }); } - protected deleteCipherWithServer(id: string, permanent: boolean, isUnassigned: boolean) { + protected deleteCipherWithServer( + id: string, + userId: UserId, + permanent: boolean, + isUnassigned: boolean, + ) { const asAdmin = this.organization?.canEditAllCiphers || isUnassigned; return permanent - ? this.cipherService.deleteWithServer(id, asAdmin) - : this.cipherService.softDeleteWithServer(id, asAdmin); + ? this.cipherService.deleteWithServer(id, userId, asAdmin) + : this.cipherService.softDeleteWithServer(id, userId, asAdmin); } protected async repromptCipher(ciphers: CipherView[]) { diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 52a22ac2946..5f39966468f 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -7,9 +7,11 @@ import { CollectionService, CollectionView } from "@bitwarden/admin-console/comm import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -45,11 +47,9 @@ export class CollectionsComponent implements OnInit { } async load() { - this.cipherDomain = await this.loadCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.cipherDomain = await this.loadCipher(activeUserId); this.collectionIds = this.loadCipherCollections(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); @@ -95,7 +95,8 @@ export class CollectionsComponent implements OnInit { } this.cipherDomain.collectionIds = selectedCollectionIds; try { - this.formPromise = this.saveCollections(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.formPromise = this.saveCollections(activeUserId); await this.formPromise; this.onSavedCollections.emit(); this.toastService.showToast({ @@ -114,8 +115,8 @@ export class CollectionsComponent implements OnInit { } } - protected loadCipher() { - return this.cipherService.get(this.cipherId); + protected loadCipher(userId: UserId) { + return this.cipherService.get(this.cipherId, userId); } protected loadCipherCollections() { @@ -129,7 +130,7 @@ export class CollectionsComponent implements OnInit { ); } - protected saveCollections() { - return this.cipherService.saveCollectionsWithServer(this.cipherDomain); + protected saveCollections(userId: UserId) { + return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId); } } diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 534a1337eda..e785441b8e4 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -73,10 +74,8 @@ export class ShareComponent implements OnInit, OnDestroy { } }); - const cipherDomain = await this.cipherService.get(this.cipherId); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); this.cipher = await cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), ); @@ -104,10 +103,8 @@ export class ShareComponent implements OnInit, OnDestroy { return; } - const cipherDomain = await this.cipherService.get(this.cipherId); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId); const cipherView = await cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), ); diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 7a0c8ae2669..923f667e680 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils"; import { EventType } from "@bitwarden/common/enums"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; @@ -101,8 +102,6 @@ export class AddEditComponent implements OnInit, OnDestroy { private personalOwnershipPolicyAppliesToActiveUser: boolean; private previousCipherId: string; - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); const creationDate = this.datePipe.transform( @@ -263,12 +262,13 @@ export class AddEditComponent implements OnInit, OnDestroy { this.title = this.i18nService.t("addItem"); } - const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + + const loadedAddEditCipherInfo = await this.loadAddEditCipherInfo(activeUserId); - const activeUserId = await firstValueFrom(this.activeUserId$); if (this.cipher == null) { if (this.editMode) { - const cipher = await this.loadCipher(); + const cipher = await this.loadCipher(activeUserId); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -420,9 +420,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.cipher.id = null; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const cipher = await this.encryptCipher(activeUserId); try { this.formPromise = this.saveCipher(cipher); @@ -516,7 +514,8 @@ export class AddEditComponent implements OnInit, OnDestroy { } try { - this.deletePromise = this.deleteCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.deletePromise = this.deleteCipher(activeUserId); await this.deletePromise; this.toastService.showToast({ variant: "success", @@ -542,7 +541,8 @@ export class AddEditComponent implements OnInit, OnDestroy { } try { - this.restorePromise = this.restoreCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.restorePromise = this.restoreCipher(activeUserId); await this.restorePromise; this.toastService.showToast({ variant: "success", @@ -725,8 +725,8 @@ export class AddEditComponent implements OnInit, OnDestroy { return allCollections.filter((c) => !c.readOnly); } - protected loadCipher() { - return this.cipherService.get(this.cipherId); + protected loadCipher(userId: UserId) { + return this.cipherService.get(this.cipherId, userId); } protected encryptCipher(userId: UserId) { @@ -746,14 +746,14 @@ export class AddEditComponent implements OnInit, OnDestroy { : this.cipherService.updateWithServer(cipher, orgAdmin); } - protected deleteCipher() { + protected deleteCipher(userId: UserId) { return this.cipher.isDeleted - ? this.cipherService.deleteWithServer(this.cipher.id, this.asAdmin) - : this.cipherService.softDeleteWithServer(this.cipher.id, this.asAdmin); + ? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin) + : this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin); } - protected restoreCipher() { - return this.cipherService.restoreWithServer(this.cipher.id, this.asAdmin); + protected restoreCipher(userId: UserId) { + return this.cipherService.restoreWithServer(this.cipher.id, userId, this.asAdmin); } /** @@ -773,8 +773,10 @@ export class AddEditComponent implements OnInit, OnDestroy { return this.ownershipOptions[0].value; } - async loadAddEditCipherInfo(): Promise { - const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$); + async loadAddEditCipherInfo(userId: UserId): Promise { + const addEditCipherInfo: any = await firstValueFrom( + this.cipherService.addEditCipherInfo$(userId), + ); const loadedSavedInfo = addEditCipherInfo != null; if (loadedSavedInfo) { @@ -787,7 +789,7 @@ export class AddEditComponent implements OnInit, OnDestroy { } } - await this.cipherService.setAddEditCipherInfo(null); + await this.cipherService.setAddEditCipherInfo(null, userId); return loadedSavedInfo; } diff --git a/libs/angular/src/vault/components/attachments.component.ts b/libs/angular/src/vault/components/attachments.component.ts index 9f1dd31da0c..b1bfcde852a 100644 --- a/libs/angular/src/vault/components/attachments.component.ts +++ b/libs/angular/src/vault/components/attachments.component.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; @@ -84,9 +85,7 @@ export class AttachmentsComponent implements OnInit { } try { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); this.formPromise = this.saveCipherAttachment(files[0], activeUserId); this.cipherDomain = await this.formPromise; this.cipher = await this.cipherDomain.decrypt( @@ -125,12 +124,11 @@ export class AttachmentsComponent implements OnInit { } try { - this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); + const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + + this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id, activeUserId); const updatedCipher = await this.deletePromises[attachment.id]; - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); const cipher = new Cipher(updatedCipher); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), @@ -228,10 +226,8 @@ export class AttachmentsComponent implements OnInit { } protected async init() { - this.cipherDomain = await this.loadCipher(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.cipherDomain = await this.loadCipher(activeUserId); this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); @@ -287,7 +283,7 @@ export class AttachmentsComponent implements OnInit { : await this.keyService.getOrgKey(this.cipher.organizationId); const decBuf = await this.encryptService.decryptToBytes(encBuf, key); const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), + this.accountService.activeAccount$.pipe(getUserId), ); this.cipherDomain = await this.cipherService.saveAttachmentRawWithServer( this.cipherDomain, @@ -301,7 +297,10 @@ export class AttachmentsComponent implements OnInit { ); // 3. Delete old - this.deletePromises[attachment.id] = this.deleteCipherAttachment(attachment.id); + this.deletePromises[attachment.id] = this.deleteCipherAttachment( + attachment.id, + activeUserId, + ); await this.deletePromises[attachment.id]; const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id); if (foundAttachment.length > 0) { @@ -335,16 +334,16 @@ export class AttachmentsComponent implements OnInit { } } - protected loadCipher() { - return this.cipherService.get(this.cipherId); + protected loadCipher(userId: UserId) { + return this.cipherService.get(this.cipherId, userId); } protected saveCipherAttachment(file: File, userId: UserId) { return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId); } - protected deleteCipherAttachment(attachmentId: string) { - return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId); + protected deleteCipherAttachment(attachmentId: string, userId: UserId) { + return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId, userId); } protected async reupload(attachment: AttachmentView) { diff --git a/libs/angular/src/vault/components/password-history.component.ts b/libs/angular/src/vault/components/password-history.component.ts index 0b385688d0b..4df9f4bd24d 100644 --- a/libs/angular/src/vault/components/password-history.component.ts +++ b/libs/angular/src/vault/components/password-history.component.ts @@ -1,9 +1,10 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { Directive, OnInit } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -39,10 +40,8 @@ export class PasswordHistoryComponent implements OnInit { } protected async init() { - const cipher = await this.cipherService.get(this.cipherId); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const cipher = await this.cipherService.get(this.cipherId, activeUserId); const decCipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); diff --git a/libs/angular/src/vault/components/vault-items.component.ts b/libs/angular/src/vault/components/vault-items.component.ts index f093aeb1330..fb76ff500eb 100644 --- a/libs/angular/src/vault/components/vault-items.component.ts +++ b/libs/angular/src/vault/components/vault-items.component.ts @@ -2,10 +2,13 @@ // @ts-strict-ignore import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; -import { BehaviorSubject, Subject, firstValueFrom, from, switchMap, takeUntil } from "rxjs"; +import { BehaviorSubject, Subject, firstValueFrom, from, map, switchMap, takeUntil } from "rxjs"; import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -41,11 +44,20 @@ export class VaultItemsComponent implements OnInit, OnDestroy { constructor( protected searchService: SearchService, protected cipherService: CipherService, + protected accountService: AccountService, ) { - this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => { - void this.doSearch(ciphers); - this.loaded = true; - }); + this.accountService.activeAccount$ + .pipe( + getUserId, + switchMap((userId) => + this.cipherService.cipherViews$(userId).pipe(map((ciphers) => ({ userId, ciphers }))), + ), + takeUntilDestroyed(), + ) + .subscribe(({ userId, ciphers }) => { + void this.doSearch(ciphers, userId); + this.loaded = true; + }); } ngOnInit(): void { @@ -122,10 +134,16 @@ export class VaultItemsComponent implements OnInit, OnDestroy { protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; - protected async doSearch(indexedCiphers?: CipherView[]) { - indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$)); + protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) { + // Get userId from activeAccount if not provided from parent stream + if (!userId) { + userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + } - const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); + indexedCiphers = + indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$(userId))); + + const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$(userId)); if (failedCiphers != null && failedCiphers.length > 0) { indexedCiphers = [...failedCiphers, ...indexedCiphers]; } diff --git a/libs/angular/src/vault/components/view.component.ts b/libs/angular/src/vault/components/view.component.ts index b746c5b0062..637596256b0 100644 --- a/libs/angular/src/vault/components/view.component.ts +++ b/libs/angular/src/vault/components/view.component.ts @@ -18,6 +18,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { TokenService } from "@bitwarden/common/auth/abstractions/token.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { EventType } from "@bitwarden/common/enums"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; @@ -29,7 +30,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; -import { CollectionId } from "@bitwarden/common/types/guid"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; @@ -79,7 +80,6 @@ export class ViewComponent implements OnDestroy, OnInit { private previousCipherId: string; private passwordReprompted = false; - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); private destroyed$ = new Subject(); get fido2CredentialCreationDateValue(): string { @@ -144,9 +144,10 @@ export class ViewComponent implements OnDestroy, OnInit { async load() { this.cleanUp(); - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); // Grab individual cipher from `cipherViews$` for the most up-to-date information - this.cipherService.cipherViews$ + this.cipherService + .cipherViews$(activeUserId) .pipe( map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)), filter((cipher) => !!cipher), @@ -250,7 +251,8 @@ export class ViewComponent implements OnDestroy, OnInit { } try { - await this.deleteCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.deleteCipher(activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -272,7 +274,8 @@ export class ViewComponent implements OnDestroy, OnInit { } try { - await this.restoreCipher(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.restoreCipher(activeUserId); this.toastService.showToast({ variant: "success", title: null, @@ -380,7 +383,8 @@ export class ViewComponent implements OnDestroy, OnInit { } if (cipherId) { - await this.cipherService.updateLastLaunchedDate(cipherId); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.updateLastLaunchedDate(cipherId, activeUserId); } this.platformUtilsService.launchUri(uri.launchUri); @@ -498,14 +502,14 @@ export class ViewComponent implements OnDestroy, OnInit { a.downloading = false; } - protected deleteCipher() { + protected deleteCipher(userId: UserId) { return this.cipher.isDeleted - ? this.cipherService.deleteWithServer(this.cipher.id) - : this.cipherService.softDeleteWithServer(this.cipher.id); + ? this.cipherService.deleteWithServer(this.cipher.id, userId) + : this.cipherService.softDeleteWithServer(this.cipher.id, userId); } - protected restoreCipher() { - return this.cipherService.restoreWithServer(this.cipher.id); + protected restoreCipher(userId: UserId) { + return this.cipherService.restoreWithServer(this.cipher.id, userId); } protected async promptPassword() { diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index aab06a69add..d175942c475 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -11,6 +11,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; @@ -30,8 +31,6 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti private readonly collapsedGroupings$: Observable> = this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( protected organizationService: OrganizationService, protected folderService: FolderService, @@ -63,7 +62,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } buildNestedFolders(organizationId?: string): Observable> { - const transformation = async (storedFolders: FolderView[]) => { + const transformation = async (storedFolders: FolderView[], userId: UserId) => { let folders: FolderView[]; // If no org or "My Vault" is selected, show all folders @@ -71,7 +70,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti folders = storedFolders; } else { // Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder - const ciphers = await this.cipherService.getAllDecrypted(); + const ciphers = await this.cipherService.getAllDecrypted(userId); const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId); folders = storedFolders.filter( (f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null, @@ -85,9 +84,13 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti }); }; - return this.activeUserId$.pipe( - switchMap((userId) => this.folderService.folderViews$(userId)), - mergeMap((folders) => from(transformation(folders))), + return this.accountService.activeAccount$.pipe( + getUserId, + switchMap((userId) => + this.folderService + .folderViews$(userId) + .pipe(mergeMap((folders) => from(transformation(folders, userId)))), + ), ); } @@ -131,7 +134,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async getFolderNested(id: string): Promise> { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const folders = await this.getAllFoldersNested( await firstValueFrom(this.folderService.folderViews$(activeUserId)), ); diff --git a/libs/common/src/platform/notifications/internal/default-notifications.service.ts b/libs/common/src/platform/notifications/internal/default-notifications.service.ts index c6b330857a4..f0586e37ff7 100644 --- a/libs/common/src/platform/notifications/internal/default-notifications.service.ts +++ b/libs/common/src/platform/notifications/internal/default-notifications.service.ts @@ -151,11 +151,15 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract await this.syncService.syncUpsertCipher( notification.payload as SyncCipherNotification, notification.type === NotificationType.SyncCipherUpdate, + payloadUserId, ); break; case NotificationType.SyncCipherDelete: case NotificationType.SyncLoginDelete: - await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); + await this.syncService.syncDeleteCipher( + notification.payload as SyncCipherNotification, + payloadUserId, + ); break; case NotificationType.SyncFolderCreate: case NotificationType.SyncFolderUpdate: diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts index 226f4c2cfe9..3ea86a1f504 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.spec.ts @@ -3,7 +3,8 @@ import { TextEncoder } from "util"; import { mock, MockProxy } from "jest-mock-extended"; import { BehaviorSubject, of } from "rxjs"; -import { Account, AccountService } from "../../../auth/abstractions/account.service"; +import { mockAccountServiceWith } from "../../../../spec"; +import { Account } from "../../../auth/abstractions/account.service"; import { UserId } from "../../../types/guid"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; @@ -46,7 +47,6 @@ describe("FidoAuthenticatorService", () => { let userInterface!: MockProxy>; let userInterfaceSession!: MockProxy; let syncService!: MockProxy; - let accountService!: MockProxy; let authenticator!: Fido2AuthenticatorService; let windowReference!: ParentWindowReference; @@ -58,7 +58,7 @@ describe("FidoAuthenticatorService", () => { syncService = mock({ activeUserLastSync$: () => of(new Date()), }); - accountService = mock(); + const accountService = mockAccountServiceWith("testId" as UserId); authenticator = new Fido2AuthenticatorService( cipherService, userInterface, diff --git a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts index 376f4dcdced..76bd19b2876 100644 --- a/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts +++ b/libs/common/src/platform/services/fido2/fido2-authenticator.service.ts @@ -1,8 +1,9 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { AccountService } from "../../../auth/abstractions/account.service"; +import { getUserId } from "../../../auth/services/account.service"; import { CipherService } from "../../../vault/abstractions/cipher.service"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type"; @@ -145,10 +146,10 @@ export class Fido2AuthenticatorService try { keyPair = await createKeyPair(); pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey); - const encrypted = await this.cipherService.get(cipherId); const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), + this.accountService.activeAccount$.pipe(getUserId), ); + const encrypted = await this.cipherService.get(cipherId, activeUserId); cipher = await encrypted.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId), @@ -309,7 +310,7 @@ export class Fido2AuthenticatorService if (selectedFido2Credential.counter > 0) { const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), + this.accountService.activeAccount$.pipe(getUserId), ); const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId); await this.cipherService.updateWithServer(encrypted); @@ -400,7 +401,8 @@ export class Fido2AuthenticatorService return []; } - const ciphers = await this.cipherService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); return ciphers .filter( (cipher) => @@ -421,7 +423,8 @@ export class Fido2AuthenticatorService return []; } - const ciphers = await this.cipherService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); return ciphers.filter( (cipher) => !cipher.isDeleted && @@ -438,7 +441,8 @@ export class Fido2AuthenticatorService } private async findCredentialsByRp(rpId: string): Promise { - const ciphers = await this.cipherService.getAllDecrypted(); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + const ciphers = await this.cipherService.getAllDecrypted(activeUserId); return ciphers.filter( (cipher) => !cipher.isDeleted && diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index cfa9030c9de..92a10baf6d2 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -129,12 +129,18 @@ export abstract class CoreSyncService implements SyncService { return this.syncCompleted(false); } - async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise { + async syncUpsertCipher( + notification: SyncCipherNotification, + isEdit: boolean, + userId: UserId, + ): Promise { this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { + + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); + if (authStatus >= AuthenticationStatus.Locked) { try { let shouldUpdate = true; - const localCipher = await this.cipherService.get(notification.id); + const localCipher = await this.cipherService.get(notification.id, userId); if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) { shouldUpdate = false; } @@ -182,7 +188,7 @@ export abstract class CoreSyncService implements SyncService { } } catch (e) { if (e != null && e.statusCode === 404 && isEdit) { - await this.cipherService.delete(notification.id); + await this.cipherService.delete(notification.id, userId); this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); return this.syncCompleted(true); } @@ -191,10 +197,12 @@ export abstract class CoreSyncService implements SyncService { return this.syncCompleted(false); } - async syncDeleteCipher(notification: SyncCipherNotification): Promise { + async syncDeleteCipher(notification: SyncCipherNotification, userId: UserId): Promise { this.syncStarted(); - if (await this.stateService.getIsAuthenticated()) { - await this.cipherService.delete(notification.id); + + const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId)); + if (authStatus >= AuthenticationStatus.Locked) { + await this.cipherService.delete(notification.id, userId); this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id }); return this.syncCompleted(true); } diff --git a/libs/common/src/platform/sync/sync.service.ts b/libs/common/src/platform/sync/sync.service.ts index 6763e01cab7..967e4db27a5 100644 --- a/libs/common/src/platform/sync/sync.service.ts +++ b/libs/common/src/platform/sync/sync.service.ts @@ -62,8 +62,9 @@ export abstract class SyncService { abstract syncUpsertCipher( notification: SyncCipherNotification, isEdit: boolean, + userId: UserId, ): Promise; - abstract syncDeleteCipher(notification: SyncFolderNotification): Promise; + abstract syncDeleteCipher(notification: SyncFolderNotification, userId: UserId): Promise; abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise; abstract syncDeleteSend(notification: SyncSendNotification): Promise; } diff --git a/libs/common/src/services/event/event-collection.service.ts b/libs/common/src/services/event/event-collection.service.ts index da38ca5bfff..b37ec0de271 100644 --- a/libs/common/src/services/event/event-collection.service.ts +++ b/libs/common/src/services/event/event-collection.service.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service"; import { EventUploadService } from "../../abstractions/event/event-upload.service"; @@ -46,7 +47,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(null, eventType, ciphers))) { + if (!(await this.shouldUpdate(userId, null, eventType, ciphers))) { return; } @@ -91,7 +92,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); - if (!(await this.shouldUpdate(organizationId, eventType, undefined, cipherId))) { + if (!(await this.shouldUpdate(userId, organizationId, eventType, undefined, cipherId))) { return; } @@ -113,18 +114,18 @@ export class EventCollectionService implements EventCollectionServiceAbstraction } /** Verifies if the event collection should be updated for the provided information + * @param userId the active user's id * @param cipherId the cipher for the event * @param organizationId the organization for the event */ private async shouldUpdate( + userId: UserId, organizationId: string = null, eventType: EventType = null, ciphers: CipherView[] = [], cipherId?: string, ): Promise { - const cipher$ = from(this.cipherService.get(cipherId)); - - const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); + const cipher$ = from(this.cipherService.get(cipherId, userId)); const orgIds$ = this.organizationService .organizations$(userId) diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 0672ae29e91..1e4275ff89b 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -19,57 +19,70 @@ import { FieldView } from "../models/view/field.view"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; export abstract class CipherService implements UserKeyRotationDataProvider { - cipherViews$: Observable; - ciphers$: Observable>; - localData$: Observable>; + abstract cipherViews$(userId: UserId): Observable; + abstract ciphers$(userId: UserId): Observable>; + abstract localData$(userId: UserId): Observable>; /** * An observable monitoring the add/edit cipher info saved to memory. */ - addEditCipherInfo$: Observable; + abstract addEditCipherInfo$(userId: UserId): Observable; /** * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. * * An empty array indicates that all ciphers were successfully decrypted. */ - failedToDecryptCiphers$: Observable; - clearCache: (userId?: string) => Promise; - encrypt: ( + abstract failedToDecryptCiphers$(userId: UserId): Observable; + abstract clearCache(userId: UserId): Promise; + abstract encrypt( model: CipherView, userId: UserId, keyForEncryption?: SymmetricCryptoKey, keyForCipherKeyDecryption?: SymmetricCryptoKey, originalCipher?: Cipher, - ) => Promise; - encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise; - encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise; - get: (id: string) => Promise; - getAll: () => Promise; - getAllDecrypted: () => Promise; - getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise; - getAllDecryptedForUrl: ( + ): Promise; + abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise; + abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise; + abstract get(id: string, userId: UserId): Promise; + abstract getAll(userId: UserId): Promise; + abstract getAllDecrypted(userId: UserId): Promise; + abstract getAllDecryptedForGrouping( + groupingId: string, + userId: UserId, + folder?: boolean, + ): Promise; + abstract getAllDecryptedForUrl( url: string, + userId: UserId, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, - ) => Promise; - filterCiphersForUrl: ( + ): Promise; + abstract filterCiphersForUrl( ciphers: CipherView[], url: string, includeOtherTypes?: CipherType[], defaultMatch?: UriMatchStrategySetting, - ) => Promise; - getAllFromApiForOrganization: (organizationId: string) => Promise; + ): Promise; + abstract getAllFromApiForOrganization(organizationId: string): Promise; /** * Gets ciphers belonging to the specified organization that the user has explicit collection level access to. * Ciphers that are not assigned to any collections are only included for users with admin access. */ - getManyFromApiForOrganization: (organizationId: string) => Promise; - getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; - getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise; - getNextCipherForUrl: (url: string) => Promise; - updateLastUsedIndexForUrl: (url: string) => void; - updateLastUsedDate: (id: string) => Promise; - updateLastLaunchedDate: (id: string) => Promise; - saveNeverDomain: (domain: string) => Promise; + abstract getManyFromApiForOrganization(organizationId: string): Promise; + abstract getLastUsedForUrl( + url: string, + userId: UserId, + autofillOnPageLoad: boolean, + ): Promise; + abstract getLastLaunchedForUrl( + url: string, + userId: UserId, + autofillOnPageLoad: boolean, + ): Promise; + abstract getNextCipherForUrl(url: string, userId: UserId): Promise; + abstract updateLastUsedIndexForUrl(url: string): void; + abstract updateLastUsedDate(id: string, userId: UserId): Promise; + abstract updateLastLaunchedDate(id: string, userId: UserId): Promise; + abstract saveNeverDomain(domain: string): Promise; /** * Create a cipher with the server * @@ -78,7 +91,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; + abstract createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise; /** * Update a cipher with the server * @param cipher The cipher to update @@ -87,88 +100,105 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; - shareWithServer: ( + abstract updateWithServer( + cipher: Cipher, + orgAdmin?: boolean, + isNotClone?: boolean, + ): Promise; + abstract shareWithServer( cipher: CipherView, organizationId: string, collectionIds: string[], userId: UserId, - ) => Promise; - shareManyWithServer: ( + ): Promise; + abstract shareManyWithServer( ciphers: CipherView[], organizationId: string, collectionIds: string[], userId: UserId, - ) => Promise; - saveAttachmentWithServer: ( + ): Promise; + abstract saveAttachmentWithServer( cipher: Cipher, unencryptedFile: any, userId: UserId, admin?: boolean, - ) => Promise; - saveAttachmentRawWithServer: ( + ): Promise; + abstract saveAttachmentRawWithServer( cipher: Cipher, filename: string, data: ArrayBuffer, userId: UserId, admin?: boolean, - ) => Promise; + ): Promise; /** * Save the collections for a cipher with the server * * @param cipher The cipher to save collections for + * @param userId The user ID * * @returns A promise that resolves when the collections have been saved */ - saveCollectionsWithServer: (cipher: Cipher) => Promise; + abstract saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise; /** * Save the collections for a cipher with the server as an admin. * Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally). * @param cipher */ - saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise; + abstract saveCollectionsWithServerAdmin(cipher: Cipher): Promise; /** * Bulk update collections for many ciphers with the server * @param orgId + * @param userId * @param cipherIds * @param collectionIds * @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added */ - bulkUpdateCollectionsWithServer: ( + abstract bulkUpdateCollectionsWithServer( orgId: OrganizationId, + userId: UserId, cipherIds: CipherId[], collectionIds: CollectionId[], removeCollections: boolean, - ) => Promise; + ): Promise; /** * Update the local store of CipherData with the provided data. Values are upserted into the existing store. * * @param cipher The cipher data to upsert. Can be a single CipherData object or an array of CipherData objects. * @returns A promise that resolves to a record of updated cipher store, keyed by their cipher ID. Returns all ciphers, not just those updated */ - upsert: (cipher: CipherData | CipherData[]) => Promise>; - replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise; - clear: (userId?: string) => Promise; - moveManyWithServer: (ids: string[], folderId: string) => Promise; - delete: (id: string | string[]) => Promise; - deleteWithServer: (id: string, asAdmin?: boolean) => Promise; - deleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise; - deleteAttachment: (id: string, revisionDate: string, attachmentId: string) => Promise; - deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise; - sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number; - sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number; - getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; - softDelete: (id: string | string[]) => Promise; - softDeleteWithServer: (id: string, asAdmin?: boolean) => Promise; - softDeleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise; - restore: ( + abstract upsert(cipher: CipherData | CipherData[]): Promise>; + abstract replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise; + abstract clear(userId?: string): Promise; + abstract moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise; + abstract delete(id: string | string[], userId: UserId): Promise; + abstract deleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise; + abstract deleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise; + abstract deleteAttachment( + id: string, + revisionDate: string, + attachmentId: string, + userId: UserId, + ): Promise; + abstract deleteAttachmentWithServer( + id: string, + attachmentId: string, + userId: UserId, + ): Promise; + abstract sortCiphersByLastUsed(a: CipherView, b: CipherView): number; + abstract sortCiphersByLastUsedThenName(a: CipherView, b: CipherView): number; + abstract getLocaleSortingFunction(): (a: CipherView, b: CipherView) => number; + abstract softDelete(id: string | string[], userId: UserId): Promise; + abstract softDeleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise; + abstract softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise; + abstract restore( cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], - ) => Promise; - restoreWithServer: (id: string, asAdmin?: boolean) => Promise; - restoreManyWithServer: (ids: string[], orgId?: string) => Promise; - getKeyForCipherKeyDecryption: (cipher: Cipher, userId: UserId) => Promise; - setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise; + userId: UserId, + ): Promise; + abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise; + abstract restoreManyWithServer(ids: string[], orgId?: string): Promise; + abstract getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise; + abstract setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId): Promise; /** * Returns user ciphers re-encrypted with the new user key. * @param originalUserKey the original user key @@ -177,11 +207,11 @@ export abstract class CipherService implements UserKeyRotationDataProvider Promise; - getNextCardCipher: () => Promise; - getNextIdentityCipher: () => Promise; + ): Promise; + abstract getNextCardCipher(userId: UserId): Promise; + abstract getNextIdentityCipher(userId: UserId): Promise; } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index c59f6672985..d87ee56ddda 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -382,8 +382,16 @@ describe("Cipher Service", () => { Cipher1: cipher1, Cipher2: cipher2, }); - cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))); - cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject([]); + jest + .spyOn(cipherService, "cipherViews$") + .mockImplementation((userId: UserId) => + decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))), + ); + + failedCiphers = new BehaviorSubject([]); + jest + .spyOn(cipherService, "failedToDecryptCiphers$") + .mockImplementation((userId: UserId) => failedCiphers); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptedKey = new EncString("Re-encrypted Cipher Key"); diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 9e06d3335c3..73d9511fb2b 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -37,7 +37,7 @@ import Domain from "../../platform/models/domain/domain-base"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncString } from "../../platform/models/domain/enc-string"; import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; -import { ActiveUserState, StateProvider } from "../../platform/state"; +import { StateProvider } from "../../platform/state"; import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid"; import { OrgKey, UserKey } from "../../types/key"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; @@ -97,33 +97,6 @@ export class CipherService implements CipherServiceAbstraction { */ private forceCipherViews$: Subject = new Subject(); - localData$: Observable>; - ciphers$: Observable>; - - /** - * Observable that emits an array of decrypted ciphers for the active user. - * This observable will not emit until the encrypted ciphers have either been loaded from state or after sync. - * - * A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that - * decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete. - * - */ - cipherViews$: Observable; - addEditCipherInfo$: Observable; - - /** - * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. - * - * An empty array indicates that all ciphers were successfully decrypted. - */ - failedToDecryptCiphers$: Observable; - - private localDataState: ActiveUserState>; - private encryptedCiphersState: ActiveUserState>; - private decryptedCiphersState: ActiveUserState>; - private failedToDecryptCiphersState: ActiveUserState; - private addEditCipherInfoState: ActiveUserState; - constructor( private keyService: KeyService, private domainSettingsService: DomainSettingsService, @@ -138,30 +111,49 @@ export class CipherService implements CipherServiceAbstraction { private configService: ConfigService, private stateProvider: StateProvider, private accountService: AccountService, - ) { - this.localDataState = this.stateProvider.getActive(LOCAL_DATA_KEY); - this.encryptedCiphersState = this.stateProvider.getActive(ENCRYPTED_CIPHERS); - this.decryptedCiphersState = this.stateProvider.getActive(DECRYPTED_CIPHERS); - this.failedToDecryptCiphersState = this.stateProvider.getActive(FAILED_DECRYPTED_CIPHERS); - this.addEditCipherInfoState = this.stateProvider.getActive(ADD_EDIT_CIPHER_INFO_KEY); + ) {} - this.localData$ = this.localDataState.state$.pipe(map((data) => data ?? {})); - this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {})); + localData$(userId: UserId): Observable> { + return this.localDataState(userId).state$.pipe(map((data) => data ?? {})); + } - // Decrypted ciphers depend on both ciphers and local data and need to be updated when either changes - this.cipherViews$ = combineLatest([this.encryptedCiphersState.state$, this.localData$]).pipe( + /** + * Observable that emits an object of encrypted ciphers for the active user. + */ + ciphers$(userId: UserId): Observable> { + return this.encryptedCiphersState(userId).state$.pipe(map((ciphers) => ciphers ?? {})); + } + + /** + * Observable that emits an array of decrypted ciphers for the active user. + * This observable will not emit until the encrypted ciphers have either been loaded from state or after sync. + * + * A `null` value indicates that the latest encrypted ciphers have not been decrypted yet and that + * decryption is in progress. The latest decrypted ciphers will be emitted once decryption is complete. + */ + cipherViews$(userId: UserId): Observable { + return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe( filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet - switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted())), + switchMap(() => merge(this.forceCipherViews$, this.getAllDecrypted(userId))), shareReplay({ bufferSize: 1, refCount: true }), ); + } - this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe( + addEditCipherInfo$(userId: UserId): Observable { + return this.addEditCipherInfoState(userId).state$; + } + + /** + * Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. + * + * An empty array indicates that all ciphers were successfully decrypted. + */ + failedToDecryptCiphers$(userId: UserId): Observable { + return this.failedToDecryptCiphersState(userId).state$.pipe( filter((ciphers) => ciphers != null), switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), shareReplay({ bufferSize: 1, refCount: true }), ); - - this.addEditCipherInfo$ = this.addEditCipherInfoState.state$; } async setDecryptedCipherCache(value: CipherView[], userId: UserId) { @@ -212,7 +204,7 @@ export class CipherService implements CipherServiceAbstraction { ): Promise { if (model.id != null) { if (originalCipher == null) { - originalCipher = await this.get(model.id); + originalCipher = await this.get(model.id, userId); } if (originalCipher != null) { await this.updateModelfromExistingCipher(model, originalCipher, userId); @@ -366,22 +358,22 @@ export class CipherService implements CipherServiceAbstraction { return ph; } - async get(id: string): Promise { - const ciphers = await firstValueFrom(this.ciphers$); + async get(id: string, userId: UserId): Promise { + const ciphers = await firstValueFrom(this.ciphers$(userId)); // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id)) { return null; } - const localData = await firstValueFrom(this.localData$); + const localData = await firstValueFrom(this.localData$(userId)); const cipherId = id as CipherId; return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null); } - async getAll(): Promise { - const localData = await firstValueFrom(this.localData$); - const ciphers = await firstValueFrom(this.ciphers$); + async getAll(userId: UserId): Promise { + const localData = await firstValueFrom(this.localData$(userId)); + const ciphers = await firstValueFrom(this.ciphers$(userId)); const response: Cipher[] = []; for (const id in ciphers) { // eslint-disable-next-line @@ -399,33 +391,27 @@ export class CipherService implements CipherServiceAbstraction { * @deprecated Use `cipherViews$` observable instead */ @sequentialize(() => "getAllDecrypted") - async getAllDecrypted(): Promise { - const decCiphers = await this.getDecryptedCiphers(); + async getAllDecrypted(userId: UserId): Promise { + const decCiphers = await this.getDecryptedCiphers(userId); if (decCiphers != null && decCiphers.length !== 0) { - await this.reindexCiphers(); - return await this.getDecryptedCiphers(); - } - - const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$); - - if (activeUserId == null) { - return []; + await this.reindexCiphers(userId); + return await this.getDecryptedCiphers(userId); } const [newDecCiphers, failedCiphers] = await this.decryptCiphers( - await this.getAll(), - activeUserId, + await this.getAll(userId), + userId, ); - await this.setDecryptedCipherCache(newDecCiphers, activeUserId); - await this.setFailedDecryptedCiphers(failedCiphers, activeUserId); + await this.setDecryptedCipherCache(newDecCiphers, userId); + await this.setFailedDecryptedCiphers(failedCiphers, userId); return newDecCiphers; } - private async getDecryptedCiphers() { + private async getDecryptedCiphers(userId: UserId) { return Object.values( - await firstValueFrom(this.decryptedCiphersState.state$.pipe(map((c) => c ?? {}))), + await firstValueFrom(this.decryptedCiphersState(userId).state$.pipe(map((c) => c ?? {}))), ); } @@ -491,18 +477,21 @@ export class CipherService implements CipherServiceAbstraction { ); } - private async reindexCiphers() { - const userId = await this.stateService.getUserId(); + private async reindexCiphers(userId: UserId) { const reindexRequired = this.searchService != null && ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; if (reindexRequired) { - await this.searchService.indexCiphers(await this.getDecryptedCiphers(), userId); + await this.searchService.indexCiphers(await this.getDecryptedCiphers(userId), userId); } } - async getAllDecryptedForGrouping(groupingId: string, folder = true): Promise { - const ciphers = await this.getAllDecrypted(); + async getAllDecryptedForGrouping( + groupingId: string, + userId: UserId, + folder = true, + ): Promise { + const ciphers = await this.getAllDecrypted(userId); return ciphers.filter((cipher) => { if (cipher.isDeleted) { @@ -524,10 +513,11 @@ export class CipherService implements CipherServiceAbstraction { async getAllDecryptedForUrl( url: string, + userId: UserId, includeOtherTypes?: CipherType[], defaultMatch: UriMatchStrategySetting = null, ): Promise { - const ciphers = await this.getAllDecrypted(); + const ciphers = await this.getAllDecrypted(userId); return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch); } @@ -569,8 +559,11 @@ export class CipherService implements CipherServiceAbstraction { }); } - private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise { - const ciphers = await this.getAllDecrypted(); + private async getAllDecryptedCiphersOfType( + type: CipherType[], + userId: UserId, + ): Promise { + const ciphers = await this.getAllDecrypted(userId); return ciphers .filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type)) .sort((a, b) => this.sortCiphersByLastUsedThenName(a, b)); @@ -613,23 +606,31 @@ export class CipherService implements CipherServiceAbstraction { return decCiphers; } - async getLastUsedForUrl(url: string, autofillOnPageLoad = false): Promise { - return this.getCipherForUrl(url, true, false, autofillOnPageLoad); + async getLastUsedForUrl( + url: string, + userId: UserId, + autofillOnPageLoad = false, + ): Promise { + return this.getCipherForUrl(url, userId, true, false, autofillOnPageLoad); } - async getLastLaunchedForUrl(url: string, autofillOnPageLoad = false): Promise { - return this.getCipherForUrl(url, false, true, autofillOnPageLoad); + async getLastLaunchedForUrl( + url: string, + userId: UserId, + autofillOnPageLoad = false, + ): Promise { + return this.getCipherForUrl(url, userId, false, true, autofillOnPageLoad); } - async getNextCipherForUrl(url: string): Promise { - return this.getCipherForUrl(url, false, false, false); + async getNextCipherForUrl(url: string, userId: UserId): Promise { + return this.getCipherForUrl(url, userId, false, false, false); } - async getNextCardCipher(): Promise { + async getNextCardCipher(userId: UserId): Promise { const cacheKey = "cardCiphers"; if (!this.sortedCiphersCache.isCached(cacheKey)) { - const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]); + const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card], userId); if (!ciphers?.length) { return null; } @@ -640,11 +641,11 @@ export class CipherService implements CipherServiceAbstraction { return this.sortedCiphersCache.getNext(cacheKey); } - async getNextIdentityCipher(): Promise { + async getNextIdentityCipher(userId: UserId): Promise { const cacheKey = "identityCiphers"; if (!this.sortedCiphersCache.isCached(cacheKey)) { - const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]); + const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity], userId); if (!ciphers?.length) { return null; } @@ -659,9 +660,8 @@ export class CipherService implements CipherServiceAbstraction { this.sortedCiphersCache.updateLastUsedIndex(url); } - async updateLastUsedDate(id: string): Promise { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - let ciphersLocalData = await firstValueFrom(this.localData$); + async updateLastUsedDate(id: string, userId: UserId): Promise { + let ciphersLocalData = await firstValueFrom(this.localData$(userId)); if (!ciphersLocalData) { ciphersLocalData = {}; @@ -676,9 +676,9 @@ export class CipherService implements CipherServiceAbstraction { }; } - await this.localDataState.update(() => ciphersLocalData); + await this.localDataState(userId).update(() => ciphersLocalData); - const decryptedCipherCache = await this.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(userId); if (!decryptedCipherCache) { return; } @@ -693,9 +693,8 @@ export class CipherService implements CipherServiceAbstraction { await this.setDecryptedCiphers(decryptedCipherCache, userId); } - async updateLastLaunchedDate(id: string): Promise { - const userId = await firstValueFrom(this.stateProvider.activeUserId$); - let ciphersLocalData = await firstValueFrom(this.localData$); + async updateLastLaunchedDate(id: string, userId: UserId): Promise { + let ciphersLocalData = await firstValueFrom(this.localData$(userId)); if (!ciphersLocalData) { ciphersLocalData = {}; @@ -707,9 +706,9 @@ export class CipherService implements CipherServiceAbstraction { lastUsedDate: currentTime, }; - await this.localDataState.update(() => ciphersLocalData); + await this.localDataState(userId).update(() => ciphersLocalData); - const decryptedCipherCache = await this.getDecryptedCiphers(); + const decryptedCipherCache = await this.getDecryptedCiphers(userId); if (!decryptedCipherCache) { return; } @@ -914,13 +913,13 @@ export class CipherService implements CipherServiceAbstraction { return new Cipher(cData); } - async saveCollectionsWithServer(cipher: Cipher): Promise { + async saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise { const request = new CipherCollectionsRequest(cipher.collectionIds); const response = await this.apiService.putCipherCollections(cipher.id, request); // The response will now check for an unavailable value. This value determines whether // the user still has Can Manage access to the item after updating. if (response.unavailable) { - await this.delete(cipher.id); + await this.delete(cipher.id, userId); return; } const data = new CipherData(response.cipher); @@ -944,6 +943,7 @@ export class CipherService implements CipherServiceAbstraction { */ async bulkUpdateCollectionsWithServer( orgId: OrganizationId, + userId: UserId, cipherIds: CipherId[], collectionIds: CollectionId[], removeCollections: boolean = false, @@ -958,7 +958,7 @@ export class CipherService implements CipherServiceAbstraction { await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false); // Update the local state - const ciphers = await firstValueFrom(this.ciphers$); + const ciphers = await firstValueFrom(this.ciphers$(userId)); for (const id of cipherIds) { const cipher = ciphers[id]; @@ -975,7 +975,7 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.encryptedCiphersState.update(() => ciphers); + await this.encryptedCiphersState(userId).update(() => ciphers); } async upsert(cipher: CipherData | CipherData[]): Promise> { @@ -1016,10 +1016,10 @@ export class CipherService implements CipherServiceAbstraction { await this.clearCache(userId); } - async moveManyWithServer(ids: string[], folderId: string): Promise { + async moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise { await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); - let ciphers = await firstValueFrom(this.ciphers$); + let ciphers = await firstValueFrom(this.ciphers$(userId)); if (ciphers == null) { ciphers = {}; } @@ -1032,11 +1032,11 @@ export class CipherService implements CipherServiceAbstraction { }); await this.clearCache(); - await this.encryptedCiphersState.update(() => ciphers); + await this.encryptedCiphersState(userId).update(() => ciphers); } - async delete(id: string | string[]): Promise { - const ciphers = await firstValueFrom(this.ciphers$); + async delete(id: string | string[], userId: UserId): Promise { + const ciphers = await firstValueFrom(this.ciphers$(userId)); if (ciphers == null) { return; } @@ -1054,35 +1054,36 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.encryptedCiphersState.update(() => ciphers); + await this.encryptedCiphersState(userId).update(() => ciphers); } - async deleteWithServer(id: string, asAdmin = false): Promise { + async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise { if (asAdmin) { await this.apiService.deleteCipherAdmin(id); } else { await this.apiService.deleteCipher(id); } - await this.delete(id); + await this.delete(id, userId); } - async deleteManyWithServer(ids: string[], asAdmin = false): Promise { + async deleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise { const request = new CipherBulkDeleteRequest(ids); if (asAdmin) { await this.apiService.deleteManyCiphersAdmin(request); } else { await this.apiService.deleteManyCiphers(request); } - await this.delete(ids); + await this.delete(ids, userId); } async deleteAttachment( id: string, revisionDate: string, attachmentId: string, + userId: UserId, ): Promise { - let ciphers = await firstValueFrom(this.ciphers$); + let ciphers = await firstValueFrom(this.ciphers$(userId)); const cipherId = id as CipherId; // eslint-disable-next-line if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) { @@ -1100,7 +1101,7 @@ export class CipherService implements CipherServiceAbstraction { ciphers[cipherId].revisionDate = revisionDate; await this.clearCache(); - await this.encryptedCiphersState.update(() => { + await this.encryptedCiphersState(userId).update(() => { if (ciphers == null) { ciphers = {}; } @@ -1110,7 +1111,11 @@ export class CipherService implements CipherServiceAbstraction { return ciphers[cipherId]; } - async deleteAttachmentWithServer(id: string, attachmentId: string): Promise { + async deleteAttachmentWithServer( + id: string, + attachmentId: string, + userId: UserId, + ): Promise { let cipherResponse = null; try { cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId); @@ -1119,7 +1124,7 @@ export class CipherService implements CipherServiceAbstraction { } const cipherData = CipherData.fromJSON(cipherResponse?.cipher); - return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId); + return await this.deleteAttachment(id, cipherData.revisionDate, attachmentId, userId); } sortCiphersByLastUsed(a: CipherView, b: CipherView): number { @@ -1192,8 +1197,8 @@ export class CipherService implements CipherServiceAbstraction { }; } - async softDelete(id: string | string[]): Promise { - let ciphers = await firstValueFrom(this.ciphers$); + async softDelete(id: string | string[], userId: UserId): Promise { + let ciphers = await firstValueFrom(this.ciphers$(userId)); if (ciphers == null) { return; } @@ -1212,7 +1217,7 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.encryptedCiphersState.update(() => { + await this.encryptedCiphersState(userId).update(() => { if (ciphers == null) { ciphers = {}; } @@ -1220,17 +1225,17 @@ export class CipherService implements CipherServiceAbstraction { }); } - async softDeleteWithServer(id: string, asAdmin = false): Promise { + async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise { if (asAdmin) { await this.apiService.putDeleteCipherAdmin(id); } else { await this.apiService.putDeleteCipher(id); } - await this.softDelete(id); + await this.softDelete(id, userId); } - async softDeleteManyWithServer(ids: string[], asAdmin = false): Promise { + async softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise { const request = new CipherBulkDeleteRequest(ids); if (asAdmin) { await this.apiService.putDeleteManyCiphersAdmin(request); @@ -1238,13 +1243,14 @@ export class CipherService implements CipherServiceAbstraction { await this.apiService.putDeleteManyCiphers(request); } - await this.softDelete(ids); + await this.softDelete(ids, userId); } async restore( cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], + userId: UserId, ) { - let ciphers = await firstValueFrom(this.ciphers$); + let ciphers = await firstValueFrom(this.ciphers$(userId)); if (ciphers == null) { return; } @@ -1265,7 +1271,7 @@ export class CipherService implements CipherServiceAbstraction { } await this.clearCache(); - await this.encryptedCiphersState.update(() => { + await this.encryptedCiphersState(userId).update(() => { if (ciphers == null) { ciphers = {}; } @@ -1273,7 +1279,7 @@ export class CipherService implements CipherServiceAbstraction { }); } - async restoreWithServer(id: string, asAdmin = false): Promise { + async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise { let response; if (asAdmin) { response = await this.apiService.putRestoreCipherAdmin(id); @@ -1281,14 +1287,14 @@ export class CipherService implements CipherServiceAbstraction { response = await this.apiService.putRestoreCipher(id); } - await this.restore({ id: id, revisionDate: response.revisionDate }); + await this.restore({ id: id, revisionDate: response.revisionDate }, userId); } /** * No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable * The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore */ - async restoreManyWithServer(ids: string[], orgId: string = null): Promise { + async restoreManyWithServer(ids: string[], userId: UserId, orgId: string = null): Promise { let response; if (orgId) { @@ -1303,7 +1309,7 @@ export class CipherService implements CipherServiceAbstraction { for (const cipher of response.data) { restores.push({ id: cipher.id, revisionDate: cipher.revisionDate }); } - await this.restore(restores); + await this.restore(restores, userId); } async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise { @@ -1313,8 +1319,8 @@ export class CipherService implements CipherServiceAbstraction { ); } - async setAddEditCipherInfo(value: AddEditCipherInfo) { - await this.addEditCipherInfoState.update(() => value, { + async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) { + await this.addEditCipherInfoState(userId).update(() => value, { shouldUpdate: (current) => !(current == null && value == null), }); } @@ -1333,8 +1339,8 @@ export class CipherService implements CipherServiceAbstraction { let encryptedCiphers: CipherWithIdRequest[] = []; - const ciphers = await firstValueFrom(this.cipherViews$); - const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$); + const ciphers = await firstValueFrom(this.cipherViews$(userId)); + const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$(userId)); if (!ciphers) { return encryptedCiphers; } @@ -1357,6 +1363,41 @@ export class CipherService implements CipherServiceAbstraction { return encryptedCiphers; } + /** + * @returns a SingleUserState + */ + private localDataState(userId: UserId) { + return this.stateProvider.getUser(userId, LOCAL_DATA_KEY); + } + + /** + * @returns a SingleUserState for the encrypted ciphers + */ + private encryptedCiphersState(userId: UserId) { + return this.stateProvider.getUser(userId, ENCRYPTED_CIPHERS); + } + + /** + * @returns a SingleUserState for the decrypted ciphers + */ + private decryptedCiphersState(userId: UserId) { + return this.stateProvider.getUser(userId, DECRYPTED_CIPHERS); + } + + /** + * @returns a SingleUserState for the add/edit cipher info + */ + private addEditCipherInfoState(userId: UserId) { + return this.stateProvider.getUser(userId, ADD_EDIT_CIPHER_INFO_KEY); + } + + /** + * @returns a SingleUserState for the failed to decrypt ciphers + */ + private failedToDecryptCiphersState(userId: UserId) { + return this.stateProvider.getUser(userId, FAILED_DECRYPTED_CIPHERS); + } + // Helpers // In the case of a cipher that is being shared with an organization, we want to decrypt the @@ -1660,6 +1701,7 @@ export class CipherService implements CipherServiceAbstraction { private async getCipherForUrl( url: string, + userId: UserId, lastUsed: boolean, lastLaunched: boolean, autofillOnPageLoad: boolean, @@ -1667,7 +1709,7 @@ export class CipherService implements CipherServiceAbstraction { const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url; if (!this.sortedCiphersCache.isCached(cacheKey)) { - let ciphers = await this.getAllDecryptedForUrl(url); + let ciphers = await this.getAllDecryptedForUrl(url, userId); if (!ciphers) { return null; } diff --git a/libs/common/src/vault/services/folder/folder.service.ts b/libs/common/src/vault/services/folder/folder.service.ts index 3d272416fbe..89bc5353f88 100644 --- a/libs/common/src/vault/services/folder/folder.service.ts +++ b/libs/common/src/vault/services/folder/folder.service.ts @@ -190,7 +190,7 @@ export class FolderService implements InternalFolderServiceAbstraction { }); // Items in a deleted folder are re-assigned to "No Folder" - const ciphers = await this.cipherService.getAll(); + const ciphers = await this.cipherService.getAll(userId); if (ciphers != null) { const updates: Cipher[] = []; for (const cId in ciphers) { diff --git a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts index 3110ebad637..2686e950974 100644 --- a/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts +++ b/libs/key-management/src/user-asymmetric-key-regeneration/services/default-user-asymmetric-key-regeneration.service.ts @@ -103,7 +103,7 @@ export class DefaultUserAsymmetricKeysRegenerationService } // The private isn't decryptable, check to see if we can decrypt something with the userKey. - const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey); + const userKeyCanDecrypt = await this.userKeyCanDecrypt(userKey, userId); if (userKeyCanDecrypt) { this.logService.info( "[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.", @@ -155,8 +155,8 @@ export class DefaultUserAsymmetricKeysRegenerationService ); } - private async userKeyCanDecrypt(userKey: UserKey): Promise { - const ciphers = await this.cipherService.getAll(); + private async userKeyCanDecrypt(userKey: UserKey, userId: UserId): Promise { + const ciphers = await this.cipherService.getAll(userId); const cipher = ciphers.find((cipher) => cipher.organizationId == null); if (cipher != null) { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index d45c39eebbd..1718bd54234 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as papa from "papaparse"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -32,8 +33,6 @@ export class IndividualVaultExportService extends BaseVaultExportService implements IndividualVaultExportServiceAbstraction { - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - constructor( private folderService: FolderService, private cipherService: CipherService, @@ -63,7 +62,7 @@ export class IndividualVaultExportService let decFolders: FolderView[] = []; let decCiphers: CipherView[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => { @@ -72,7 +71,7 @@ export class IndividualVaultExportService ); promises.push( - this.cipherService.getAllDecrypted().then((ciphers) => { + this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => { decCiphers = ciphers.filter((f) => f.deletedDate == null); }), ); @@ -90,7 +89,7 @@ export class IndividualVaultExportService let folders: Folder[] = []; let ciphers: Cipher[] = []; const promises = []; - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => { @@ -99,7 +98,7 @@ export class IndividualVaultExportService ); promises.push( - this.cipherService.getAll().then((c) => { + this.cipherService.getAll(activeUserId).then((c) => { ciphers = c.filter((f) => f.deletedDate == null); }), ); @@ -107,7 +106,7 @@ export class IndividualVaultExportService await Promise.all(promises); const userKey = await this.keyService.getUserKeyWithLegacySupport( - await firstValueFrom(this.activeUserId$), + await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)), ); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 86c19950b7c..0961347664d 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -1,7 +1,7 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import * as papa from "papaparse"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { CollectionService, @@ -13,6 +13,7 @@ import { import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; @@ -94,9 +95,7 @@ export class OrganizationVaultExportService const decCollections: CollectionView[] = []; const decCiphers: CipherView[] = []; const promises = []; - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.apiService.getOrganizationExport(organizationId).then((exportData) => { @@ -184,6 +183,7 @@ export class OrganizationVaultExportService let allDecCiphers: CipherView[] = []; let decCollections: CollectionView[] = []; const promises = []; + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.collectionService.getAllDecrypted().then(async (collections) => { @@ -192,7 +192,7 @@ export class OrganizationVaultExportService ); promises.push( - this.cipherService.getAllDecrypted().then((ciphers) => { + this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => { allDecCiphers = ciphers; }), ); @@ -216,6 +216,7 @@ export class OrganizationVaultExportService let allCiphers: Cipher[] = []; let encCollections: Collection[] = []; const promises = []; + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); promises.push( this.collectionService.getAll().then((collections) => { @@ -224,7 +225,7 @@ export class OrganizationVaultExportService ); promises.push( - this.cipherService.getAll().then((ciphers) => { + this.cipherService.getAll(activeUserId).then((ciphers) => { allCiphers = ciphers; }), ); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts index 1ee9a985f5a..ce12ca95e1e 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.spec.ts @@ -110,7 +110,7 @@ describe("CipherAttachmentsComponent", () => { it("fetches cipherView using `cipherId`", async () => { await component.ngOnInit(); - expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333"); + expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333", mockUserId); expect(component.cipher).toEqual(cipherView); }); diff --git a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts index e366cdab3fe..7e26e8afae9 100644 --- a/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/cipher-attachments.component.ts @@ -21,10 +21,11 @@ import { ReactiveFormsModule, Validators, } from "@angular/forms"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; @@ -118,10 +119,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit { } async ngOnInit(): Promise { - this.cipherDomain = await this.cipherService.get(this.cipherId); - this.activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + this.cipherDomain = await this.cipherService.get(this.cipherId, this.activeUserId); this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), ); diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts index 8e0d4f7a665..4442fa6e75d 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.spec.ts @@ -2,12 +2,16 @@ import { ComponentFixture, TestBed } from "@angular/core/testing"; import { By } from "@angular/platform-browser"; import { mock } from "jest-mock-extended"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { DialogService, ToastService } from "@bitwarden/components"; +import { mockAccountServiceWith } from "../../../../../../common/spec"; + import { DeleteAttachmentComponent } from "./delete-attachment.component"; describe("DeleteAttachmentComponent", () => { @@ -42,6 +46,7 @@ describe("DeleteAttachmentComponent", () => { }, { provide: I18nService, useValue: { t: (key: string) => key } }, { provide: LogService, useValue: mock() }, + { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) }, ], }) .overrideProvider(DialogService, { @@ -90,7 +95,11 @@ describe("DeleteAttachmentComponent", () => { }); // Called with cipher id and attachment id - expect(deleteAttachmentWithServer).toHaveBeenCalledWith("5555-444-3333", "222-3333-4444"); + expect(deleteAttachmentWithServer).toHaveBeenCalledWith( + "5555-444-3333", + "222-3333-4444", + "UserId", + ); }); it("shows toast message on successful deletion", async () => { diff --git a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts index b1ada907b1d..d7d6b550665 100644 --- a/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts +++ b/libs/vault/src/cipher-form/components/attachments/delete-attachment/delete-attachment.component.ts @@ -1,7 +1,10 @@ import { CommonModule } from "@angular/common"; import { Component, EventEmitter, Input, Output } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; @@ -36,6 +39,7 @@ export class DeleteAttachmentComponent { private cipherService: CipherService, private logService: LogService, private dialogService: DialogService, + private accountService: AccountService, ) {} delete = async () => { @@ -50,7 +54,19 @@ export class DeleteAttachmentComponent { } try { - await this.cipherService.deleteAttachmentWithServer(this.cipherId, this.attachment.id); + const activeUserId = await firstValueFrom( + this.accountService.activeAccount$.pipe(getOptionalUserId), + ); + + if (activeUserId == null) { + throw new Error("An active user is expected while deleting an attachment."); + } + + await this.cipherService.deleteAttachmentWithServer( + this.cipherId, + this.attachment.id, + activeUserId, + ); this.toastService.showToast({ variant: "success", diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 28b13b51c61..b9add41c222 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -34,14 +35,12 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { private collectionService: CollectionService = inject(CollectionService); private accountService = inject(AccountService); - private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id)); - async buildConfig( mode: CipherFormMode, cipherId?: CipherId, cipherType?: CipherType, ): Promise { - const activeUserId = await firstValueFrom(this.activeUserId$); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const [organizations, collections, allowPersonalOwnership, folders, cipher] = await firstValueFrom( @@ -62,7 +61,7 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { ), ), ), - this.getCipher(cipherId), + this.getCipher(activeUserId, cipherId), ]), ); @@ -94,10 +93,10 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { .policyAppliesToActiveUser$(PolicyType.PersonalOwnership) .pipe(map((p) => !p)); - private getCipher(id?: CipherId): Promise { + private getCipher(userId: UserId, id?: CipherId): Promise { if (id == null) { return Promise.resolve(null); } - return this.cipherService.get(id); + return this.cipherService.get(id, userId); } } diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 059214cc185..98286e4bbb2 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -1,10 +1,11 @@ // FIXME: Update this file to be type safe and remove this and next line // @ts-strict-ignore import { inject, Injectable } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -23,9 +24,7 @@ export class DefaultCipherFormService implements CipherFormService { private apiService: ApiService = inject(ApiService); async decryptCipher(cipher: Cipher): Promise { - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); return await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -33,9 +32,7 @@ export class DefaultCipherFormService implements CipherFormService { async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise { // Passing the original cipher is important here as it is responsible for appending to password history - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const encryptedCipher = await this.cipherService.encrypt( cipher, activeUserId, @@ -90,7 +87,10 @@ export class DefaultCipherFormService implements CipherFormService { // When using an admin config or the cipher was unassigned, update collections as an admin savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher); } else { - savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher); + savedCipher = await this.cipherService.saveCollectionsWithServer( + encryptedCipher, + activeUserId, + ); } } diff --git a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts index e7deb78c868..9b12139b00e 100644 --- a/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts +++ b/libs/vault/src/cipher-view/autofill-options/autofill-options-view.component.ts @@ -2,8 +2,11 @@ // @ts-strict-ignore import { CommonModule } from "@angular/common"; import { Component, Input } from "@angular/core"; +import { firstValueFrom } from "rxjs"; import { JslibModule } from "@bitwarden/angular/jslib.module"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; @@ -38,10 +41,12 @@ export class AutofillOptionsViewComponent { constructor( private platformUtilsService: PlatformUtilsService, private cipherService: CipherService, + private accountService: AccountService, ) {} async openWebsite(selectedUri: string) { - await this.cipherService.updateLastLaunchedDate(this.cipherId); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + await this.cipherService.updateLastLaunchedDate(this.cipherId, activeUserId); this.platformUtilsService.launchUri(selectedUri); } } diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index a8ef3557385..76a6a1b10a6 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -179,7 +179,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI private get selectedOrgId(): OrganizationId { return this.formGroup.getRawValue().selectedOrg || this.params.organizationId; } - private activeUserId: UserId; private destroy$ = new Subject(); constructor( @@ -193,10 +192,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI ) {} async ngOnInit() { - this.activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null); if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) { @@ -253,12 +248,15 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI .filter((i) => i.organizationId) .map((i) => i.id as CipherId); + const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + // Move personal items to the organization if (this.personalItemsCount > 0) { await this.moveToOrganization( this.selectedOrgId, this.params.ciphers.filter((c) => c.organizationId == null), this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), + activeUserId, ); } @@ -267,8 +265,8 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI // Update assigned collections for single org cipher or bulk update collections for multiple org ciphers await (isSingleOrgCipher - ? this.updateAssignedCollections(this.editableItems[0]) - : this.bulkUpdateCollections(cipherIds)); + ? this.updateAssignedCollections(this.editableItems[0], activeUserId) + : this.bulkUpdateCollections(cipherIds, activeUserId)); this.toastService.showToast({ variant: "success", @@ -447,12 +445,13 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI organizationId: OrganizationId, shareableCiphers: CipherView[], selectedCollectionIds: CollectionId[], + userId: UserId, ) { await this.cipherService.shareManyWithServer( shareableCiphers, organizationId, selectedCollectionIds, - this.activeUserId, + userId, ); this.toastService.showToast({ @@ -465,10 +464,11 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI }); } - private async bulkUpdateCollections(cipherIds: CipherId[]) { + private async bulkUpdateCollections(cipherIds: CipherId[], userId: UserId) { if (this.formGroup.controls.collections.value.length > 0) { await this.cipherService.bulkUpdateCollectionsWithServer( this.selectedOrgId, + userId, cipherIds, this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), false, @@ -483,6 +483,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI ) { await this.cipherService.bulkUpdateCollectionsWithServer( this.selectedOrgId, + userId, cipherIds, [this.params.activeCollection.id as CollectionId], true, @@ -490,14 +491,14 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI } } - private async updateAssignedCollections(cipherView: CipherView) { + private async updateAssignedCollections(cipherView: CipherView, userId: UserId) { const { collections } = this.formGroup.getRawValue(); cipherView.collectionIds = collections.map((i) => i.id as CollectionId); - const cipher = await this.cipherService.encrypt(cipherView, this.activeUserId); + const cipher = await this.cipherService.encrypt(cipherView, userId); if (this.params.isSingleCipherAdmin) { await this.cipherService.saveCollectionsWithServerAdmin(cipher); } else { - await this.cipherService.saveCollectionsWithServer(cipher); + await this.cipherService.saveCollectionsWithServer(cipher, userId); } } }