1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-14 07:13:32 +00:00

[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
This commit is contained in:
SmithThe4th
2025-02-12 08:53:31 -05:00
committed by GitHub
parent e45ef6b924
commit a2945203f4
98 changed files with 1174 additions and 725 deletions

View File

@@ -825,6 +825,7 @@ describe("NotificationBackground", () => {
queueMessage.newPassword, queueMessage.newPassword,
message.edit, message.edit,
sender.tab, sender.tab,
"testId",
); );
expect(updateWithServerSpy).toHaveBeenCalled(); expect(updateWithServerSpy).toHaveBeenCalled();
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
@@ -862,6 +863,7 @@ describe("NotificationBackground", () => {
queueMessage.password, queueMessage.password,
message.edit, message.edit,
sender.tab, sender.tab,
"testId",
); );
expect(editItemSpy).not.toHaveBeenCalled(); expect(editItemSpy).not.toHaveBeenCalled();
expect(createWithServerSpy).not.toHaveBeenCalled(); expect(createWithServerSpy).not.toHaveBeenCalled();
@@ -895,6 +897,7 @@ describe("NotificationBackground", () => {
queueMessage.newPassword, queueMessage.newPassword,
message.edit, message.edit,
sender.tab, sender.tab,
"testId",
); );
expect(editItemSpy).toHaveBeenCalled(); expect(editItemSpy).toHaveBeenCalled();
expect(updateWithServerSpy).not.toHaveBeenCalled(); expect(updateWithServerSpy).not.toHaveBeenCalled();
@@ -904,10 +907,13 @@ describe("NotificationBackground", () => {
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "editedCipher", command: "editedCipher",
}); });
expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith({ expect(setAddEditCipherInfoSpy).toHaveBeenCalledWith(
{
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); },
"testId",
);
expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, { expect(openAddEditVaultItemPopoutSpy).toHaveBeenCalledWith(sender.tab, {
cipherId: cipherView.id, cipherId: cipherView.id,
}); });
@@ -945,7 +951,7 @@ describe("NotificationBackground", () => {
queueMessage, queueMessage,
message.folder, message.folder,
); );
expect(editItemSpy).toHaveBeenCalledWith(cipherView, sender.tab); expect(editItemSpy).toHaveBeenCalledWith(cipherView, "testId", sender.tab);
expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, { expect(tabSendMessageSpy).toHaveBeenCalledWith(sender.tab, {
command: "closeNotificationBar", command: "closeNotificationBar",
}); });

View File

@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { import {
ExtensionCommand, ExtensionCommand,
ExtensionCommandType, ExtensionCommandType,
@@ -22,6 +23,7 @@ import { EnvironmentService } from "@bitwarden/common/platform/abstractions/envi
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -87,8 +89,6 @@ export default class NotificationBackground {
bgGetDecryptedCiphers: () => this.getNotificationCipherData(), bgGetDecryptedCiphers: () => this.getNotificationCipherData(),
}; };
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private autofillService: AutofillService, private autofillService: AutofillService,
private cipherService: CipherService, private cipherService: CipherService,
@@ -151,7 +151,13 @@ export default class NotificationBackground {
firstValueFrom(this.environmentService.environment$), firstValueFrom(this.environmentService.environment$),
]); ]);
const iconsServerUrl = env.getIconsUrl(); 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) => { return decryptedCiphers.map((view) => {
const { id, name, reprompt, favorite, login } = view; const { id, name, reprompt, favorite, login } = view;
@@ -304,7 +310,14 @@ export default class NotificationBackground {
return; 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( const usernameMatches = ciphers.filter(
(c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername, (c) => c.login.username != null && c.login.username.toLowerCase() === normalizedUsername,
); );
@@ -382,7 +395,14 @@ export default class NotificationBackground {
} }
let id: string = null; 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) { if (changeData.currentPassword != null) {
const passwordMatches = ciphers.filter( const passwordMatches = ciphers.filter(
(c) => c.login.password === changeData.currentPassword, (c) => c.login.password === changeData.currentPassword,
@@ -535,37 +555,42 @@ export default class NotificationBackground {
this.notificationQueue.splice(i, 1); this.notificationQueue.splice(i, 1);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (queueMessage.type === NotificationQueueMessageType.ChangePassword) { if (queueMessage.type === NotificationQueueMessageType.ChangePassword) {
const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId); const cipherView = await this.getDecryptedCipherById(queueMessage.cipherId, activeUserId);
await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab); await this.updatePassword(cipherView, queueMessage.newPassword, edit, tab, activeUserId);
return; return;
} }
// If the vault was locked, check if a cipher needs updating instead of creating a new one // If the vault was locked, check if a cipher needs updating instead of creating a new one
if (queueMessage.wasVaultLocked) { if (queueMessage.wasVaultLocked) {
const allCiphers = await this.cipherService.getAllDecryptedForUrl(queueMessage.uri); const allCiphers = await this.cipherService.getAllDecryptedForUrl(
queueMessage.uri,
activeUserId,
);
const existingCipher = allCiphers.find( const existingCipher = allCiphers.find(
(c) => (c) =>
c.login.username != null && c.login.username.toLowerCase() === queueMessage.username, c.login.username != null && c.login.username.toLowerCase() === queueMessage.username,
); );
if (existingCipher != null) { if (existingCipher != null) {
await this.updatePassword(existingCipher, queueMessage.password, edit, tab); await this.updatePassword(existingCipher, queueMessage.password, edit, tab, activeUserId);
return; return;
} }
} }
folderId = (await this.folderExists(folderId)) ? folderId : null; folderId = (await this.folderExists(folderId, activeUserId)) ? folderId : null;
const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId); const newCipher = this.convertAddLoginQueueMessageToCipherView(queueMessage, folderId);
if (edit) { if (edit) {
await this.editItem(newCipher, tab); await this.editItem(newCipher, activeUserId, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
return; return;
} }
const activeUserId = await firstValueFrom(this.activeUserId$);
const cipher = await this.cipherService.encrypt(newCipher, activeUserId); const cipher = await this.cipherService.encrypt(newCipher, activeUserId);
try { try {
await this.cipherService.createWithServer(cipher); await this.cipherService.createWithServer(cipher);
@@ -588,24 +613,25 @@ export default class NotificationBackground {
* @param newPassword - The new password to update the cipher with * @param newPassword - The new password to update the cipher with
* @param edit - Identifies if the cipher should be edited or simply updated * @param edit - Identifies if the cipher should be edited or simply updated
* @param tab - The tab that the message was sent from * @param tab - The tab that the message was sent from
* @param userId - The active account user ID
*/ */
private async updatePassword( private async updatePassword(
cipherView: CipherView, cipherView: CipherView,
newPassword: string, newPassword: string,
edit: boolean, edit: boolean,
tab: chrome.tabs.Tab, tab: chrome.tabs.Tab,
userId: UserId,
) { ) {
cipherView.login.password = newPassword; cipherView.login.password = newPassword;
if (edit) { if (edit) {
await this.editItem(cipherView, tab); await this.editItem(cipherView, userId, tab);
await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" }); await BrowserApi.tabSendMessage(tab, { command: "closeNotificationBar" });
await BrowserApi.tabSendMessage(tab, { command: "editedCipher" }); await BrowserApi.tabSendMessage(tab, { command: "editedCipher" });
return; return;
} }
const activeUserId = await firstValueFrom(this.activeUserId$); const cipher = await this.cipherService.encrypt(cipherView, userId);
const cipher = await this.cipherService.encrypt(cipherView, activeUserId);
try { try {
// We've only updated the password, no need to broadcast editedCipher message // We've only updated the password, no need to broadcast editedCipher message
await this.cipherService.updateWithServer(cipher); await this.cipherService.updateWithServer(cipher);
@@ -622,33 +648,34 @@ export default class NotificationBackground {
* and opens the add/edit vault item popout. * and opens the add/edit vault item popout.
* *
* @param cipherView - The cipher to edit * @param cipherView - The cipher to edit
* @param userId - The active account user ID
* @param senderTab - The tab that the message was sent from * @param senderTab - The tab that the message was sent from
*/ */
private async editItem(cipherView: CipherView, senderTab: chrome.tabs.Tab) { private async editItem(cipherView: CipherView, userId: UserId, senderTab: chrome.tabs.Tab) {
await this.cipherService.setAddEditCipherInfo({ await this.cipherService.setAddEditCipherInfo(
{
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); },
userId,
);
await this.openAddEditVaultItemPopout(senderTab, { cipherId: cipherView.id }); 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") { if (Utils.isNullOrWhitespace(folderId) || folderId === "null") {
return false; return false;
} }
const activeUserId = await firstValueFrom(this.activeUserId$); const folders = await firstValueFrom(this.folderService.folderViews$(userId));
const folders = await firstValueFrom(this.folderService.folderViews$(activeUserId));
return folders.some((x) => x.id === folderId); return folders.some((x) => x.id === folderId);
} }
private async getDecryptedCipherById(cipherId: string) { private async getDecryptedCipherById(cipherId: string, userId: UserId) {
const cipher = await this.cipherService.get(cipherId); const cipher = await this.cipherService.get(cipherId, userId);
if (cipher != null && cipher.type === CipherType.Login) { if (cipher != null && cipher.type === CipherType.Login) {
const activeUserId = await firstValueFrom(this.activeUserId$);
return await cipher.decrypt( return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, userId),
); );
} }
return null; return null;
@@ -685,7 +712,9 @@ export default class NotificationBackground {
* Returns the first value found from the folder service's folderViews$ observable. * Returns the first value found from the folder service's folderViews$ observable.
*/ */
private async getFolderData() { 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)); return await firstValueFrom(this.folderService.folderViews$(activeUserId));
} }

View File

@@ -206,6 +206,7 @@ describe("OverlayBackground", () => {
inlineMenuFieldQualificationService, inlineMenuFieldQualificationService,
themeStateService, themeStateService,
totpService, totpService,
accountService,
generatedPasswordCallbackMock, generatedPasswordCallbackMock,
addPasswordCallbackMock, addPasswordCallbackMock,
); );
@@ -849,7 +850,7 @@ describe("OverlayBackground", () => {
await flushPromises(); await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [
CipherType.Card, CipherType.Card,
CipherType.Identity, CipherType.Identity,
]); ]);
@@ -872,7 +873,7 @@ describe("OverlayBackground", () => {
await flushPromises(); await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId);
expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(cipherService.sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual( expect(overlayBackground["inlineMenuCiphers"]).toStrictEqual(
new Map([ new Map([
@@ -891,7 +892,7 @@ describe("OverlayBackground", () => {
await flushPromises(); await flushPromises();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, [ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId, [
CipherType.Card, CipherType.Card,
CipherType.Identity, CipherType.Identity,
]); ]);

View File

@@ -13,8 +13,10 @@ import {
} from "rxjs"; } from "rxjs";
import { parse } from "tldts"; import { parse } from "tldts";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId, getUserId } from "@bitwarden/common/auth/services/account.service";
import { import {
AutofillOverlayVisibility, AutofillOverlayVisibility,
SHOW_AUTOFILL_BUTTON, 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { ThemeStateService } from "@bitwarden/common/platform/theming/theme-state.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.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 inlineMenuFieldQualificationService: InlineMenuFieldQualificationService,
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
private totpService: TotpService, private totpService: TotpService,
private accountService: AccountService,
private generatePasswordCallback: () => Promise<string>, private generatePasswordCallback: () => Promise<string>,
private addPasswordCallback: (password: string) => Promise<void>, private addPasswordCallback: (password: string) => Promise<void>,
) { ) {
@@ -405,13 +409,20 @@ export class OverlayBackground implements OverlayBackgroundInterface {
currentTab: chrome.tabs.Tab, currentTab: chrome.tabs.Tab,
updateAllCipherTypes: boolean, updateAllCipherTypes: boolean,
): Promise<CipherView[]> { ): Promise<CipherView[]> {
if (updateAllCipherTypes || !this.cardAndIdentityCiphers) { const activeUserId = await firstValueFrom(
return this.getAllCipherTypeViews(currentTab); this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (!activeUserId) {
return [];
} }
const cipherViews = (await this.cipherService.getAllDecryptedForUrl(currentTab.url || "")).sort( if (updateAllCipherTypes || !this.cardAndIdentityCiphers) {
(a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b), 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 return this.cardAndIdentityCiphers
? cipherViews.concat(...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. * Queries all cipher types from the user's vault returns them sorted by last used.
* *
* @param currentTab - The current tab * @param currentTab - The current tab
* @param userId - The active user id
*/ */
private async getAllCipherTypeViews(currentTab: chrome.tabs.Tab): Promise<CipherView[]> { private async getAllCipherTypeViews(
currentTab: chrome.tabs.Tab,
userId: UserId,
): Promise<CipherView[]> {
if (!this.cardAndIdentityCiphers) { if (!this.cardAndIdentityCiphers) {
this.cardAndIdentityCiphers = new Set([]); this.cardAndIdentityCiphers = new Set([]);
} }
this.cardAndIdentityCiphers.clear(); this.cardAndIdentityCiphers.clear();
const cipherViews = ( const cipherViews = (
await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", [ await this.cipherService.getAllDecryptedForUrl(currentTab.url || "", userId, [
CipherType.Card, CipherType.Card,
CipherType.Identity, CipherType.Identity,
]) ])
@@ -2399,10 +2414,14 @@ export class OverlayBackground implements OverlayBackgroundInterface {
try { try {
this.closeInlineMenu(sender); this.closeInlineMenu(sender);
await this.cipherService.setAddEditCipherInfo({ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.cipherService.setAddEditCipherInfo(
{
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); },
activeUserId,
);
await this.openAddEditVaultItemPopout(sender.tab, { await this.openAddEditVaultItemPopout(sender.tab, {
cipherId: cipherView.id, cipherId: cipherView.id,

View File

@@ -1,7 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -14,6 +18,7 @@ export default class WebRequestBackground {
platformUtilsService: PlatformUtilsService, platformUtilsService: PlatformUtilsService,
private cipherService: CipherService, private cipherService: CipherService,
private authService: AuthService, private authService: AuthService,
private accountService: AccountService,
private readonly webRequest: typeof chrome.webRequest, private readonly webRequest: typeof chrome.webRequest,
) { ) {
this.isFirefox = platformUtilsService.isFirefox(); this.isFirefox = platformUtilsService.isFirefox();
@@ -55,7 +60,16 @@ export default class WebRequestBackground {
// eslint-disable-next-line // eslint-disable-next-line
private async resolveAuthCredentials(domain: string, success: Function, error: Function) { 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(); error();
return; return;
} }
@@ -63,6 +77,7 @@ export default class WebRequestBackground {
try { try {
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
domain, domain,
activeUserId,
null, null,
UriMatchStrategy.Host, UriMatchStrategy.Host,
); );

View File

@@ -2,6 +2,8 @@ import { mock, MockProxy } from "jest-mock-extended";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "@bitwarden/common/vault/enums/cipher-reprompt-type";
@@ -14,6 +16,9 @@ describe("CipherContextMenuHandler", () => {
let authService: MockProxy<AuthService>; let authService: MockProxy<AuthService>;
let cipherService: MockProxy<CipherService>; let cipherService: MockProxy<CipherService>;
const mockUserId = "UserId" as UserId;
const accountService = mockAccountServiceWith(mockUserId);
let sut: CipherContextMenuHandler; let sut: CipherContextMenuHandler;
beforeEach(() => { beforeEach(() => {
@@ -24,7 +29,12 @@ describe("CipherContextMenuHandler", () => {
jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue(); jest.spyOn(MainContextMenuHandler, "removeAll").mockResolvedValue();
sut = new CipherContextMenuHandler(mainContextMenuHandler, authService, cipherService); sut = new CipherContextMenuHandler(
mainContextMenuHandler,
authService,
cipherService,
accountService,
);
}); });
afterEach(() => jest.resetAllMocks()); afterEach(() => jest.resetAllMocks());
@@ -119,10 +129,11 @@ describe("CipherContextMenuHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", [ expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
CipherType.Card, "https://test.com",
CipherType.Identity, mockUserId,
]); [CipherType.Card, CipherType.Identity],
);
expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3); expect(mainContextMenuHandler.loadOptions).toHaveBeenCalledTimes(3);

View File

@@ -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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -14,6 +18,7 @@ export class CipherContextMenuHandler {
private mainContextMenuHandler: MainContextMenuHandler, private mainContextMenuHandler: MainContextMenuHandler,
private authService: AuthService, private authService: AuthService,
private cipherService: CipherService, private cipherService: CipherService,
private accountService: AccountService,
) {} ) {}
async update(url: string) { async update(url: string) {
@@ -35,7 +40,14 @@ export class CipherContextMenuHandler {
return; 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.Card,
CipherType.Identity, CipherType.Identity,
]); ]);

View File

@@ -61,6 +61,8 @@ describe("ContextMenuClickedHandler", () => {
return cipherView; return cipherView;
}; };
const mockUserId = "UserId" as UserId;
let copyToClipboard: CopyToClipboardAction; let copyToClipboard: CopyToClipboardAction;
let generatePasswordToClipboard: GeneratePasswordToClipboardAction; let generatePasswordToClipboard: GeneratePasswordToClipboardAction;
let autofill: AutofillAction; let autofill: AutofillAction;
@@ -79,7 +81,7 @@ describe("ContextMenuClickedHandler", () => {
autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>(); autofill = jest.fn<Promise<void>, [tab: chrome.tabs.Tab, cipher: CipherView]>();
authService = mock(); authService = mock();
cipherService = mock(); cipherService = mock();
accountService = mockAccountServiceWith("userId" as UserId); accountService = mockAccountServiceWith(mockUserId as UserId);
totpService = mock(); totpService = mock();
eventCollectionService = mock(); eventCollectionService = mock();
@@ -191,7 +193,11 @@ describe("ContextMenuClickedHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
"https://test.com",
mockUserId,
[],
);
expect(copyToClipboard).toHaveBeenCalledTimes(1); expect(copyToClipboard).toHaveBeenCalledTimes(1);
@@ -215,7 +221,11 @@ describe("ContextMenuClickedHandler", () => {
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledTimes(1);
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith("https://test.com", []); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(
"https://test.com",
mockUserId,
[],
);
}); });
}); });
}); });

View File

@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { import {
AUTOFILL_CARD_ID, AUTOFILL_CARD_ID,
AUTOFILL_ID, AUTOFILL_ID,
@@ -105,6 +106,13 @@ export class ContextMenuClickedHandler {
menuItemId as string, menuItemId as string,
); );
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId == null) {
return;
}
if (isCreateCipherAction) { if (isCreateCipherAction) {
// pass; defer to logic below // pass; defer to logic below
} else if (menuItemId === NOOP_COMMAND_SUFFIX) { } else if (menuItemId === NOOP_COMMAND_SUFFIX) {
@@ -120,12 +128,13 @@ export class ContextMenuClickedHandler {
// in scenarios like unlock on autofill // in scenarios like unlock on autofill
const ciphers = await this.cipherService.getAllDecryptedForUrl( const ciphers = await this.cipherService.getAllDecryptedForUrl(
tab.url, tab.url,
activeUserId,
additionalCiphersToGet, additionalCiphersToGet,
); );
cipher = ciphers[0]; cipher = ciphers[0];
} else { } else {
const ciphers = await this.cipherService.getAllDecrypted(); const ciphers = await this.cipherService.getAllDecrypted(activeUserId);
cipher = ciphers.find(({ id }) => id === menuItemId); cipher = ciphers.find(({ id }) => id === menuItemId);
} }
@@ -133,9 +142,6 @@ export class ContextMenuClickedHandler {
return; return;
} }
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
await this.accountService.setAccountActivity(activeUserId, new Date()); await this.accountService.setAccountActivity(activeUserId, new Date());
switch (info.parentMenuItemId) { switch (info.parentMenuItemId) {
case AUTOFILL_ID: case AUTOFILL_ID:

View File

@@ -110,6 +110,7 @@ describe("OverlayBackground", () => {
i18nService, i18nService,
platformUtilsService, platformUtilsService,
themeStateService, themeStateService,
accountService,
); );
jest jest
@@ -205,7 +206,7 @@ describe("OverlayBackground", () => {
await overlayBackground.updateOverlayCiphers(); await overlayBackground.updateOverlayCiphers();
expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled(); expect(BrowserApi.getTabFromCurrentWindowId).toHaveBeenCalled();
expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url); expect(cipherService.getAllDecryptedForUrl).toHaveBeenCalledWith(url, mockUserId);
expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled(); expect(overlayBackground["cipherService"].sortCiphersByLastUsedThenName).toHaveBeenCalled();
expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual( expect(overlayBackground["overlayLoginCiphers"]).toStrictEqual(
new Map([ new Map([

View File

@@ -1,7 +1,8 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @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 { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants"; import { SHOW_AUTOFILL_BUTTON } from "@bitwarden/common/autofill/constants";
@@ -106,6 +107,7 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
private i18nService: I18nService, private i18nService: I18nService,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private themeStateService: ThemeStateService, private themeStateService: ThemeStateService,
private accountService: AccountService,
) {} ) {}
/** /**
@@ -152,9 +154,13 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
} }
this.overlayLoginCiphers = new Map(); 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++) { for (let cipherIndex = 0; cipherIndex < ciphersViews.length; cipherIndex++) {
this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]); this.overlayLoginCiphers.set(`overlay-cipher-${cipherIndex}`, ciphersViews[cipherIndex]);
} }
@@ -660,10 +666,16 @@ class LegacyOverlayBackground implements OverlayBackgroundInterface {
cipherView.type = CipherType.Login; cipherView.type = CipherType.Login;
cipherView.login = loginView; cipherView.login = loginView;
await this.cipherService.setAddEditCipherInfo({ const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
await this.cipherService.setAddEditCipherInfo(
{
cipher: cipherView, cipher: cipherView,
collectionIds: cipherView.collectionIds, collectionIds: cipherView.collectionIds,
}); },
activeUserId,
);
await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id }); await this.openAddEditVaultItemPopout(sender.tab, { cipherId: cipherView.id });
await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher"); await BrowserApi.sendMessage("inlineAutofillMenuRefreshAddEditCipher");

View File

@@ -20,6 +20,7 @@ import {
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; import { SearchService } from "@bitwarden/common/abstractions/search.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { DomainSettingsService } from "@bitwarden/common/autofill/services/domain-settings.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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.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, (cipher) => cipher.type === CipherType.Login && !cipher.isDeleted,
); );
@@ -211,7 +215,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all( this.ciphers = await Promise.all(
message.cipherIds.map(async (cipherId) => { message.cipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId); const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt( return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
@@ -232,7 +236,7 @@ export class Fido2Component implements OnInit, OnDestroy {
this.ciphers = await Promise.all( this.ciphers = await Promise.all(
message.existingCipherIds.map(async (cipherId) => { message.existingCipherIds.map(async (cipherId) => {
const cipher = await this.cipherService.get(cipherId); const cipher = await this.cipherService.get(cipherId, activeUserId);
return cipher.decrypt( return cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );

View File

@@ -769,7 +769,10 @@ describe("AutofillService", () => {
); );
expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled(); expect(autofillService["generateLoginFillScript"]).toHaveBeenCalled();
expect(logService.info).not.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( expect(chrome.tabs.sendMessage).toHaveBeenCalledWith(
autofillOptions.pageDetails[0].tab.id, autofillOptions.pageDetails[0].tab.id,
{ {
@@ -1030,8 +1033,8 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, false); const result = await autofillService.doAutoFillOnTab(pageDetails, tab, false);
expect(cipherService.getNextCipherForUrl).not.toHaveBeenCalled(); expect(cipherService.getNextCipherForUrl).not.toHaveBeenCalled();
expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, true); expect(cipherService.getLastLaunchedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, true); expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(autofillService.doAutoFill).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull(); expect(result).toBeNull();
}); });
@@ -1044,7 +1047,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); 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.getLastLaunchedForUrl).not.toHaveBeenCalled();
expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled(); expect(cipherService.getLastUsedForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).not.toHaveBeenCalled();
@@ -1074,7 +1077,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); 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.getLastUsedForUrl).not.toHaveBeenCalled();
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled(); expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({ expect(autofillService.doAutoFill).toHaveBeenCalledWith({
@@ -1104,8 +1107,8 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); 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).toHaveBeenCalledWith(tab.url, true); expect(cipherService.getLastUsedForUrl).toHaveBeenCalledWith(tab.url, mockUserId, true);
expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled(); expect(cipherService.updateLastUsedIndexForUrl).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).toHaveBeenCalledWith({ expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab, tab: tab,
@@ -1132,7 +1135,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, fromCommand); 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(cipherService.updateLastUsedIndexForUrl).toHaveBeenCalledWith(tab.url);
expect(autofillService.doAutoFill).toHaveBeenCalledWith({ expect(autofillService.doAutoFill).toHaveBeenCalledWith({
tab: tab, tab: tab,
@@ -1163,7 +1166,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); 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(userVerificationService.hasMasterPasswordAndMasterKeyHash).toHaveBeenCalled();
expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, { expect(autofillService["openVaultItemPasswordRepromptPopout"]).toHaveBeenCalledWith(tab, {
cipherId: cipher.id, cipherId: cipher.id,
@@ -1189,7 +1192,7 @@ describe("AutofillService", () => {
const result = await autofillService.doAutoFillOnTab(pageDetails, tab, true); 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["openVaultItemPasswordRepromptPopout"]).not.toHaveBeenCalled();
expect(autofillService.doAutoFill).not.toHaveBeenCalled(); expect(autofillService.doAutoFill).not.toHaveBeenCalled();
expect(result).toBeNull(); expect(result).toBeNull();

View File

@@ -8,6 +8,7 @@ import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction"; import { UserVerificationService } from "@bitwarden/common/auth/abstractions/user-verification/user-verification.service.abstraction";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { getOptionalUserId } from "@bitwarden/common/auth/services/account.service";
import { import {
AutofillOverlayVisibility, AutofillOverlayVisibility,
CardExpiryDateDelimiters, CardExpiryDateDelimiters,
@@ -464,7 +465,7 @@ export default class AutofillService implements AutofillServiceInterface {
didAutofill = true; didAutofill = true;
if (!options.skipLastUsed) { 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. // 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, autoSubmitLogin = false,
): Promise<string | null> { ): Promise<string | null> {
let cipher: CipherView; let cipher: CipherView;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId == null) {
return null;
}
if (fromCommand) { if (fromCommand) {
cipher = await this.cipherService.getNextCipherForUrl(tab.url); cipher = await this.cipherService.getNextCipherForUrl(tab.url, activeUserId);
} else { } else {
const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(tab.url, true); const lastLaunchedCipher = await this.cipherService.getLastLaunchedForUrl(
tab.url,
activeUserId,
true,
);
if ( if (
lastLaunchedCipher && lastLaunchedCipher &&
Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000 Date.now().valueOf() - lastLaunchedCipher.localData?.lastLaunched?.valueOf() < 30000
) { ) {
cipher = lastLaunchedCipher; cipher = lastLaunchedCipher;
} else { } 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 cipher: CipherView;
let cacheKey = ""; let cacheKey = "";
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(getOptionalUserId),
);
if (activeUserId == null) {
return null;
}
if (cipherType === CipherType.Card) { if (cipherType === CipherType.Card) {
cacheKey = "cardCiphers"; cacheKey = "cardCiphers";
cipher = await this.cipherService.getNextCardCipher(); cipher = await this.cipherService.getNextCardCipher(activeUserId);
} else { } else {
cacheKey = "identityCiphers"; cacheKey = "identityCiphers";
cipher = await this.cipherService.getNextIdentityCipher(); cipher = await this.cipherService.getNextIdentityCipher(activeUserId);
} }
if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) { if (!cipher || !cacheKey || (cipher.reprompt === CipherRepromptType.Password && !fromCommand)) {

View File

@@ -1258,6 +1258,7 @@ export default class MainBackground {
this.mainContextMenuHandler, this.mainContextMenuHandler,
this.authService, this.authService,
this.cipherService, this.cipherService,
this.accountService,
); );
if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) { if (chrome.webRequest != null && chrome.webRequest.onAuthRequired != null) {
@@ -1265,6 +1266,7 @@ export default class MainBackground {
this.platformUtilsService, this.platformUtilsService,
this.cipherService, this.cipherService,
this.authService, this.authService,
this.accountService,
chrome.webRequest, chrome.webRequest,
); );
} }
@@ -1636,6 +1638,7 @@ export default class MainBackground {
this.i18nService, this.i18nService,
this.platformUtilsService, this.platformUtilsService,
this.themeStateService, this.themeStateService,
this.accountService,
); );
} else { } else {
this.overlayBackground = new OverlayBackground( this.overlayBackground = new OverlayBackground(
@@ -1653,6 +1656,7 @@ export default class MainBackground {
this.inlineMenuFieldQualificationService, this.inlineMenuFieldQualificationService,
this.themeStateService, this.themeStateService,
this.totpService, this.totpService,
this.accountService,
() => this.generatePassword(), () => this.generatePassword(),
(password) => this.addPasswordToHistory(password), (password) => this.addPasswordToHistory(password),
); );

View File

@@ -2,8 +2,10 @@
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { BadgeSettingsServiceAbstraction } from "@bitwarden/common/autofill/services/badge-settings.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -22,6 +24,7 @@ export class UpdateBadge {
private authService: AuthService; private authService: AuthService;
private badgeSettingsService: BadgeSettingsServiceAbstraction; private badgeSettingsService: BadgeSettingsServiceAbstraction;
private cipherService: CipherService; private cipherService: CipherService;
private accountService: AccountService;
private badgeAction: typeof chrome.action | typeof chrome.browserAction; private badgeAction: typeof chrome.action | typeof chrome.browserAction;
private sidebarAction: OperaSidebarAction | FirefoxSidebarAction; private sidebarAction: OperaSidebarAction | FirefoxSidebarAction;
private win: Window & typeof globalThis; private win: Window & typeof globalThis;
@@ -34,6 +37,7 @@ export class UpdateBadge {
this.badgeSettingsService = services.badgeSettingsService; this.badgeSettingsService = services.badgeSettingsService;
this.authService = services.authService; this.authService = services.authService;
this.cipherService = services.cipherService; this.cipherService = services.cipherService;
this.accountService = services.accountService;
} }
async run(opts?: { tabId?: number; windowId?: number }): Promise<void> { async run(opts?: { tabId?: number; windowId?: number }): Promise<void> {
@@ -87,7 +91,14 @@ export class UpdateBadge {
return; 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(); let countText = ciphers.length == 0 ? "" : ciphers.length.toString();
if (ciphers.length > 9) { if (ciphers.length > 9) {
countText = "9+"; countText = "9+";

View File

@@ -179,7 +179,7 @@ export class AppComponent implements OnInit, OnDestroy {
await this.clearComponentStates(); await this.clearComponentStates();
} }
if (url.startsWith("/tabs/")) { if (url.startsWith("/tabs/")) {
await this.cipherService.setAddEditCipherInfo(null); await this.cipherService.setAddEditCipherInfo(null, this.activeUserId);
} }
(window as any).previousPopupUrl = url; (window as any).previousPopupUrl = url;

View File

@@ -1,14 +1,17 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute, Router } from "@angular/router"; import { ActivatedRoute, Router } from "@angular/router";
import { mock, MockProxy } from "jest-mock-extended"; 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 { 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 { EventType } from "@bitwarden/common/enums";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
@@ -58,9 +61,9 @@ describe("AddEditV2Component", () => {
collect.mockClear(); collect.mockClear();
addEditCipherInfo$ = new BehaviorSubject<AddEditCipherInfo | null>(null); addEditCipherInfo$ = new BehaviorSubject<AddEditCipherInfo | null>(null);
cipherServiceMock = mock<CipherService>(); cipherServiceMock = mock<CipherService>({
cipherServiceMock.addEditCipherInfo$ = addEditCipherInfo$: jest.fn().mockReturnValue(addEditCipherInfo$),
addEditCipherInfo$.asObservable() as Observable<AddEditCipherInfo>; });
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
imports: [AddEditV2Component], imports: [AddEditV2Component],
@@ -81,6 +84,7 @@ describe("AddEditV2Component", () => {
canDeleteCipher$: jest.fn().mockReturnValue(true), canDeleteCipher$: jest.fn().mockReturnValue(true),
}, },
}, },
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
], ],
}) })
.overrideProvider(CipherFormConfigService, { .overrideProvider(CipherFormConfigService, {

View File

@@ -9,10 +9,12 @@ import { firstValueFrom, map, Observable, switchMap } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.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 { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -180,6 +182,7 @@ export class AddEditV2Component implements OnInit {
private toastService: ToastService, private toastService: ToastService,
private dialogService: DialogService, private dialogService: DialogService,
protected cipherAuthorizationService: CipherAuthorizationService, protected cipherAuthorizationService: CipherAuthorizationService,
private accountService: AccountService,
) { ) {
this.subscribeToParams(); this.subscribeToParams();
} }
@@ -281,9 +284,15 @@ export class AddEditV2Component implements OnInit {
config.initialValues = this.setInitialValuesFromParams(params); 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 // 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 // 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) { if (cachedCipherInfo != null) {
// Cached cipher info has priority over queryParams // Cached cipher info has priority over queryParams
@@ -292,7 +301,7 @@ export class AddEditV2Component implements OnInit {
...mapAddEditCipherInfoToInitialValues(cachedCipherInfo), ...mapAddEditCipherInfoToInitialValues(cachedCipherInfo),
}; };
// Be sure to clear the "cached" cipher info, so it doesn't get used again // 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) { if (["edit", "partial-edit"].includes(config.mode) && config.originalCipher?.id) {
@@ -371,7 +380,8 @@ export class AddEditV2Component implements OnInit {
} }
try { try {
await this.deleteCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.deleteCipher(activeUserId);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
return false; return false;
@@ -388,10 +398,10 @@ export class AddEditV2Component implements OnInit {
return true; return true;
}; };
protected deleteCipher() { protected deleteCipher(userId: UserId) {
return this.config.originalCipher.deletedDate return this.config.originalCipher.deletedDate
? this.cipherService.deleteWithServer(this.config.originalCipher.id) ? this.cipherService.deleteWithServer(this.config.originalCipher.id, userId)
: this.cipherService.softDeleteWithServer(this.config.originalCipher.id); : this.cipherService.softDeleteWithServer(this.config.originalCipher.id, userId);
} }
} }

View File

@@ -5,12 +5,13 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { ReactiveFormsModule } from "@angular/forms"; import { ReactiveFormsModule } from "@angular/forms";
import { ActivatedRoute } from "@angular/router"; 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 { CollectionService } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { OrganizationId } from "@bitwarden/common/types/guid"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { import {
@@ -58,16 +59,19 @@ export class AssignCollections {
private accountService: AccountService, private accountService: AccountService,
route: ActivatedRoute, route: ActivatedRoute,
) { ) {
const cipher$: Observable<CipherView> = route.queryParams.pipe( const cipher$: Observable<CipherView> = this.accountService.activeAccount$.pipe(
switchMap(({ cipherId }) => this.cipherService.get(cipherId)),
switchMap((cipherDomain) =>
this.accountService.activeAccount$.pipe(
map((account) => account?.id), map((account) => account?.id),
filter((userId) => userId != null),
switchMap((userId) => switchMap((userId) =>
this.cipherService route.queryParams.pipe(
.getKeyForCipherKeyDecryption(cipherDomain, userId) switchMap(async ({ cipherId }) => {
.then(cipherDomain.decrypt.bind(cipherDomain)), const cipherDomain = await this.cipherService.get(cipherId, userId);
), const key: UserKey | OrgKey = await this.cipherService.getKeyForCipherKeyDecryption(
cipherDomain,
userId,
);
return cipherDomain.decrypt(key);
}),
), ),
), ),
); );

View File

@@ -77,10 +77,10 @@ export class OpenAttachmentsComponent implements OnInit {
return; return;
} }
const cipherDomain = await this.cipherService.get(this.cipherId);
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)), this.accountService.activeAccount$.pipe(map((a) => a?.id)),
); );
const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
const cipher = await cipherDomain.decrypt( const cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
); );

View File

@@ -59,7 +59,9 @@ describe("VaultHeaderV2Component", () => {
providers: [ providers: [
{ {
provide: CipherService, provide: CipherService,
useValue: mock<CipherService>({ cipherViews$: new BehaviorSubject([]) }), useValue: mock<CipherService>({
cipherViews$: jest.fn().mockReturnValue(new BehaviorSubject([])),
}),
}, },
{ provide: VaultSettingsService, useValue: mock<VaultSettingsService>() }, { provide: VaultSettingsService, useValue: mock<VaultSettingsService>() },
{ provide: FolderService, useValue: mock<FolderService>() }, { provide: FolderService, useValue: mock<FolderService>() },

View File

@@ -22,6 +22,8 @@ import { Router } from "@angular/router";
import { firstValueFrom, Observable, map } from "rxjs"; import { firstValueFrom, Observable, map } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
@@ -265,6 +267,7 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
private router: Router, private router: Router,
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private dialogService: DialogService, private dialogService: DialogService,
private accountService: AccountService,
) {} ) {}
ngOnInit(): void { ngOnInit(): void {
@@ -311,7 +314,8 @@ export class VaultListItemsContainerComponent implements OnInit, AfterViewInit {
this.viewCipherTimeout = null; 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); await BrowserApi.createNewTab(cipher.login.launchUri);

View File

@@ -1,13 +1,15 @@
import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing"; import { ComponentFixture, fakeAsync, TestBed, tick } from "@angular/core/testing";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { mock } from "jest-mock-extended"; import { mock } from "jest-mock-extended";
import { BehaviorSubject, Subject } from "rxjs"; import { Subject } from "rxjs";
import { WINDOW } from "@bitwarden/angular/services/injection-tokens"; 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 { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -19,6 +21,7 @@ import { PasswordHistoryV2Component } from "./vault-password-history-v2.componen
describe("PasswordHistoryV2Component", () => { describe("PasswordHistoryV2Component", () => {
let fixture: ComponentFixture<PasswordHistoryV2Component>; let fixture: ComponentFixture<PasswordHistoryV2Component>;
const params$ = new Subject(); const params$ = new Subject();
const mockUserId = "acct-1" as UserId;
const mockCipherView = { const mockCipherView = {
id: "111-222-333", id: "111-222-333",
@@ -45,9 +48,7 @@ describe("PasswordHistoryV2Component", () => {
{ provide: CipherService, useValue: mock<CipherService>({ get: getCipher }) }, { provide: CipherService, useValue: mock<CipherService>({ get: getCipher }) },
{ {
provide: AccountService, provide: AccountService,
useValue: mock<AccountService>({ useValue: mockAccountServiceWith(mockUserId),
activeAccount$: new BehaviorSubject({ id: "acct-1" } as Account),
}),
}, },
{ provide: PopupRouterCacheService, useValue: { back } }, { provide: PopupRouterCacheService, useValue: { back } },
{ provide: ActivatedRoute, useValue: { queryParams: params$ } }, { provide: ActivatedRoute, useValue: { queryParams: params$ } },
@@ -64,7 +65,7 @@ describe("PasswordHistoryV2Component", () => {
tick(100); tick(100);
expect(getCipher).toHaveBeenCalledWith(mockCipherView.id); expect(getCipher).toHaveBeenCalledWith(mockCipherView.id, mockUserId);
})); }));
it("navigates back when a cipherId is not in the params", () => { it("navigates back when a cipherId is not in the params", () => {

View File

@@ -58,8 +58,6 @@ export class PasswordHistoryV2Component implements OnInit {
/** Load the cipher based on the given Id */ /** Load the cipher based on the given Id */
private async loadCipher(cipherId: string) { private async loadCipher(cipherId: string) {
const cipher = await this.cipherService.get(cipherId);
const activeAccount = await firstValueFrom( const activeAccount = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a: { id: string | undefined }) => a)), 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 activeUserId = activeAccount.id as UserId;
const cipher = await this.cipherService.get(cipherId, activeUserId);
this.cipher = await cipher.decrypt( this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );

View File

@@ -3,9 +3,20 @@ import { CommonModule } from "@angular/common";
import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core"; import { AfterViewInit, Component, DestroyRef, OnDestroy, OnInit, ViewChild } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { RouterLink } from "@angular/router"; 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 { 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 { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -96,6 +107,7 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
private vaultPopupItemsService: VaultPopupItemsService, private vaultPopupItemsService: VaultPopupItemsService,
private vaultPopupListFiltersService: VaultPopupListFiltersService, private vaultPopupListFiltersService: VaultPopupListFiltersService,
private vaultScrollPositionService: VaultPopupScrollPositionService, private vaultScrollPositionService: VaultPopupScrollPositionService,
private accountService: AccountService,
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
private cipherService: CipherService, private cipherService: CipherService,
private dialogService: DialogService, private dialogService: DialogService,
@@ -136,7 +148,10 @@ export class VaultV2Component implements OnInit, AfterViewInit, OnDestroy {
} }
async ngOnInit() { async ngOnInit() {
this.cipherService.failedToDecryptCiphers$ const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.cipherService
.failedToDecryptCiphers$(activeUserId)
.pipe( .pipe(
map((ciphers) => ciphers.filter((c) => !c.isDeleted)), map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
filter((ciphers) => ciphers.length > 0), filter((ciphers) => ciphers.length > 0),

View File

@@ -150,7 +150,7 @@ describe("ViewV2Component", () => {
flush(); // Resolve all promises flush(); // Resolve all promises
expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444"); expect(mockCipherService.get).toHaveBeenCalledWith("122-333-444", mockUserId);
expect(component.cipher).toEqual(mockCipher); expect(component.cipher).toEqual(mockCipher);
})); }));

View File

@@ -5,13 +5,14 @@ import { Component } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
import { FormsModule } from "@angular/forms"; import { FormsModule } from "@angular/forms";
import { ActivatedRoute, Router } from "@angular/router"; 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 { CollectionView } from "@bitwarden/admin-console/common";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { import {
AUTOFILL_ID, AUTOFILL_ID,
COPY_PASSWORD_ID, COPY_PASSWORD_ID,
@@ -22,6 +23,7 @@ import {
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service"; import { PremiumUpgradePromptService } from "@bitwarden/common/vault/abstractions/premium-upgrade-prompt.service";
import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service"; import { ViewPasswordHistoryService } from "@bitwarden/common/vault/abstractions/view-password-history.service";
@@ -87,6 +89,8 @@ type LoadAction =
], ],
}) })
export class ViewV2Component { export class ViewV2Component {
private activeUserId: UserId;
headerText: string; headerText: string;
cipher: CipherView; cipher: CipherView;
organization$: Observable<Organization>; organization$: Observable<Organization>;
@@ -117,14 +121,20 @@ export class ViewV2Component {
subscribeToParams(): void { subscribeToParams(): void {
this.route.queryParams this.route.queryParams
.pipe( .pipe(
switchMap(async (params): Promise<CipherView> => { switchMap(async (params) => {
this.loadAction = params.action; this.loadAction = params.action;
this.senderTabId = params.senderTabId ? parseInt(params.senderTabId, 10) : undefined; 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.cipher = cipher;
this.headerText = this.setHeader(cipher.type); this.headerText = this.setHeader(cipher.type);
this.activeUserId = activeUserId;
if (this.loadAction) { if (this.loadAction) {
await this._handleLoadAction(this.loadAction, this.senderTabId); await this._handleLoadAction(this.loadAction, this.senderTabId);
@@ -159,13 +169,10 @@ export class ViewV2Component {
} }
} }
async getCipherData(id: string) { async getCipherData(id: string, userId: UserId) {
const cipher = await this.cipherService.get(id); const cipher = await this.cipherService.get(id, userId);
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
return await cipher.decrypt( 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<void> => { restore = async (): Promise<void> => {
try { try {
await this.cipherService.restoreWithServer(this.cipher.id); await this.cipherService.restoreWithServer(this.cipher.id, this.activeUserId);
} catch (e) { } catch (e) {
this.logService.error(e); this.logService.error(e);
} }
@@ -228,8 +235,8 @@ export class ViewV2Component {
protected deleteCipher() { protected deleteCipher() {
return this.cipher.isDeleted return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id) ? this.cipherService.deleteWithServer(this.cipher.id, this.activeUserId)
: this.cipherService.softDeleteWithServer(this.cipher.id); : this.cipherService.softDeleteWithServer(this.cipher.id, this.activeUserId);
} }
protected showFooter(): boolean { protected showFooter(): boolean {

View File

@@ -15,6 +15,8 @@ import { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service";
import { CipherType } from "@bitwarden/common/vault/enums"; 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 { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service"; import { InlineMenuFieldQualificationService } from "../../../autofill/services/inline-menu-field-qualification.service";
@@ -34,6 +36,10 @@ describe("VaultPopupItemsService", () => {
let mockCollections: CollectionView[]; let mockCollections: CollectionView[];
let activeUserLastSync$: BehaviorSubject<Date>; let activeUserLastSync$: BehaviorSubject<Date>;
let ciphersSubject: BehaviorSubject<Record<CipherId, CipherData>>;
let localDataSubject: BehaviorSubject<Record<CipherId, LocalData>>;
let failedToDecryptCiphersSubject: BehaviorSubject<CipherView[]>;
const cipherServiceMock = mock<CipherService>(); const cipherServiceMock = mock<CipherService>();
const vaultSettingsServiceMock = mock<VaultSettingsService>(); const vaultSettingsServiceMock = mock<VaultSettingsService>();
const organizationServiceMock = mock<OrganizationService>(); const organizationServiceMock = mock<OrganizationService>();
@@ -60,9 +66,21 @@ describe("VaultPopupItemsService", () => {
cipherList[3].favorite = true; cipherList[3].favorite = true;
cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList); cipherServiceMock.getAllDecrypted.mockResolvedValue(cipherList);
cipherServiceMock.ciphers$ = new BehaviorSubject(null);
cipherServiceMock.localData$ = new BehaviorSubject(null); ciphersSubject = new BehaviorSubject<Record<CipherId, CipherData>>({});
cipherServiceMock.failedToDecryptCiphers$ = new BehaviorSubject([]); localDataSubject = new BehaviorSubject<Record<CipherId, LocalData>>({});
failedToDecryptCiphersSubject = new BehaviorSubject<CipherView[]>([]);
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); searchService.searchCiphers.mockImplementation(async (_, __, ciphers) => ciphers);
cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) => cipherServiceMock.filterCiphersForUrl.mockImplementation(async (ciphers) =>
ciphers.filter((c) => ["0", "1"].includes(c.id)), ciphers.filter((c) => ["0", "1"].includes(c.id)),
@@ -118,6 +136,7 @@ describe("VaultPopupItemsService", () => {
{ provide: CollectionService, useValue: collectionService }, { provide: CollectionService, useValue: collectionService },
{ provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock },
{ provide: SyncService, useValue: syncServiceMock }, { provide: SyncService, useValue: syncServiceMock },
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
{ {
provide: InlineMenuFieldQualificationService, provide: InlineMenuFieldQualificationService,
useValue: inlineMenuFieldQualificationServiceMock, useValue: inlineMenuFieldQualificationServiceMock,
@@ -155,7 +174,7 @@ describe("VaultPopupItemsService", () => {
await tracker.expectEmission(); await tracker.expectEmission();
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null); ciphersSubject.next({});
await tracker.expectEmission(); await tracker.expectEmission();
@@ -169,7 +188,7 @@ describe("VaultPopupItemsService", () => {
await tracker.expectEmission(); await tracker.expectEmission();
(cipherServiceMock.localData$ as BehaviorSubject<any>).next(null); localDataSubject.next({});
await tracker.expectEmission(); await tracker.expectEmission();
@@ -373,7 +392,7 @@ describe("VaultPopupItemsService", () => {
cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers); cipherServiceMock.getAllDecrypted.mockResolvedValue(ciphers);
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null); ciphersSubject.next({});
const deletedCiphers = await firstValueFrom(service.deletedCiphers$); const deletedCiphers = await firstValueFrom(service.deletedCiphers$);
expect(deletedCiphers.length).toBe(1); expect(deletedCiphers.length).toBe(1);
@@ -422,7 +441,7 @@ describe("VaultPopupItemsService", () => {
it("should cycle when cipherService.ciphers$ emits", async () => { it("should cycle when cipherService.ciphers$ emits", async () => {
// Restart tracking // Restart tracking
tracked = new ObservableTracker(service.loading$); tracked = new ObservableTracker(service.loading$);
(cipherServiceMock.ciphers$ as BehaviorSubject<any>).next(null); ciphersSubject.next({});
await trackedCiphers.pauseUntilReceived(2); await trackedCiphers.pauseUntilReceived(2);

View File

@@ -1,6 +1,6 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { inject, Injectable, NgZone } from "@angular/core"; import { Injectable, NgZone } from "@angular/core";
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
@@ -86,16 +86,19 @@ export class VaultPopupItemsService {
* Observable that contains the list of all decrypted ciphers. * Observable that contains the list of all decrypted ciphers.
* @private * @private
*/ */
private _allDecryptedCiphers$: Observable<CipherView[]> = merge( private _allDecryptedCiphers$: Observable<CipherView[]> = this.accountService.activeAccount$.pipe(
this.cipherService.ciphers$, map((a) => a?.id),
this.cipherService.localData$, filter((userId) => userId != null),
).pipe( switchMap((userId) =>
runInsideAngular(inject(NgZone)), // Workaround to ensure cipher$ state provider emissions are run inside Angular merge(this.cipherService.ciphers$(userId), this.cipherService.localData$(userId)).pipe(
runInsideAngular(this.ngZone),
tap(() => this._ciphersLoading$.next()), tap(() => this._ciphersLoading$.next()),
waitUntilSync(this.syncService), waitUntilSync(this.syncService),
switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted())), switchMap(() => Utils.asyncToObservable(() => this.cipherService.getAllDecrypted(userId))),
withLatestFrom(this.cipherService.failedToDecryptCiphers$), withLatestFrom(this.cipherService.failedToDecryptCiphers$(userId)),
map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]), map(([ciphers, failedToDecryptCiphers]) => [...failedToDecryptCiphers, ...ciphers]),
),
),
shareReplay({ refCount: true, bufferSize: 1 }), shareReplay({ refCount: true, bufferSize: 1 }),
); );
@@ -281,6 +284,7 @@ export class VaultPopupItemsService {
private vaultPopupAutofillService: VaultPopupAutofillService, private vaultPopupAutofillService: VaultPopupAutofillService,
private syncService: SyncService, private syncService: SyncService,
private accountService: AccountService, private accountService: AccountService,
private ngZone: NgZone,
) {} ) {}
applyFilter(newSearchText: string) { applyFilter(newSearchText: string) {

View File

@@ -41,7 +41,7 @@ describe("VaultPopupListFiltersService", () => {
} as unknown as FolderService; } as unknown as FolderService;
const cipherService = { const cipherService = {
cipherViews$, cipherViews$: () => cipherViews$,
} as unknown as CipherService; } as unknown as CipherService;
const organizationService = { const organizationService = {

View File

@@ -93,16 +93,6 @@ export class VaultPopupListFiltersService {
*/ */
private cipherViews: CipherView[] = []; private cipherViews: CipherView[] = [];
/**
* Observable of cipher views
*/
private cipherViews$: Observable<CipherView[]> = 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)); private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
@@ -271,8 +261,16 @@ export class VaultPopupListFiltersService {
* Folder array structured to be directly passed to `ChipSelectComponent` * Folder array structured to be directly passed to `ChipSelectComponent`
*/ */
folders$: Observable<ChipSelectOption<FolderView>[]> = this.activeUserId$.pipe( folders$: Observable<ChipSelectOption<FolderView>[]> = this.activeUserId$.pipe(
switchMap((userId) => switchMap((userId) => {
combineLatest([ // 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( this.filters$.pipe(
distinctUntilChanged( distinctUntilChanged(
(previousFilter, currentFilter) => (previousFilter, currentFilter) =>
@@ -281,7 +279,7 @@ export class VaultPopupListFiltersService {
), ),
), ),
this.folderService.folderViews$(userId), this.folderService.folderViews$(userId),
this.cipherViews$, cipherViews$,
]).pipe( ]).pipe(
map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => { map(([filters, folders, cipherViews]): [PopupListFilter, FolderView[], CipherView[]] => {
if (folders.length === 1 && folders[0].id === null) { if (folders.length === 1 && folders[0].id === null) {
@@ -330,8 +328,8 @@ export class VaultPopupListFiltersService {
map((folders) => map((folders) =>
folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")), folders.nestedList.map((f) => this.convertToChipSelectOption(f, "bwi-folder")),
), ),
), );
), }),
); );
/** /**

View File

@@ -3,8 +3,11 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { ChangeDetectionStrategy, Component, Input } from "@angular/core"; import { ChangeDetectionStrategy, Component, Input } from "@angular/core";
import { Router } from "@angular/router"; import { Router } from "@angular/router";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId } from "@bitwarden/common/types/guid"; import { CipherId } from "@bitwarden/common/types/guid";
@@ -65,6 +68,7 @@ export class TrashListItemsContainerComponent {
private i18nService: I18nService, private i18nService: I18nService,
private dialogService: DialogService, private dialogService: DialogService,
private passwordRepromptService: PasswordRepromptService, private passwordRepromptService: PasswordRepromptService,
private accountService: AccountService,
private router: Router, private router: Router,
) {} ) {}
@@ -81,7 +85,8 @@ export class TrashListItemsContainerComponent {
async restore(cipher: CipherView) { async restore(cipher: CipherView) {
try { 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"]); await this.router.navigate(["/trash"]);
this.toastService.showToast({ this.toastService.showToast({
@@ -112,7 +117,8 @@ export class TrashListItemsContainerComponent {
} }
try { 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"]); await this.router.navigate(["/trash"]);
this.toastService.showToast({ this.toastService.showToast({

View File

@@ -1,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Response } from "../../models/response"; import { Response } from "../../models/response";
@@ -48,7 +49,9 @@ export class ShareCommand {
organizationId = organizationId.toLowerCase(); 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) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -56,15 +59,12 @@ export class ShareCommand {
return Response.badRequest("This item already belongs to an organization."); 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( const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
try { try {
await this.cipherService.shareWithServer(cipherView, organizationId, req, activeUserId); 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( const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
); );

View File

@@ -1,11 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { CollectionRequest } from "@bitwarden/admin-console/common"; import { CollectionRequest } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request"; import { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; import { CipherExport } from "@bitwarden/common/models/export/cipher.export";
import { CollectionExport } from "@bitwarden/common/models/export/collection.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"; import { FolderResponse } from "../vault/models/folder.response";
export class EditCommand { export class EditCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
@@ -85,14 +84,12 @@ export class EditCommand {
} }
private async editCipher(id: string, req: CipherExport) { 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) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
let cipherView = await cipher.decrypt( let cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
@@ -114,7 +111,9 @@ export class EditCommand {
} }
private async editCipherCollections(id: string, req: string[]) { 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) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -129,11 +128,14 @@ export class EditCommand {
cipher.collectionIds = req; cipher.collectionIds = req;
try { try {
const updatedCipher = await this.cipherService.saveCollectionsWithServer(cipher); const updatedCipher = await this.cipherService.saveCollectionsWithServer(
cipher,
activeUserId,
);
const decCipher = await updatedCipher.decrypt( const decCipher = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption( await this.cipherService.getKeyForCipherKeyDecryption(
updatedCipher, updatedCipher,
await firstValueFrom(this.activeUserId$), await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)),
), ),
); );
const res = new CipherResponse(decCipher); const res = new CipherResponse(decCipher);
@@ -144,7 +146,7 @@ export class EditCommand {
} }
private async editFolder(id: string, req: FolderExport) { 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); const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) { if (folder == null) {
return Response.notFound(); return Response.notFound();

View File

@@ -52,8 +52,6 @@ import { FolderResponse } from "../vault/models/folder.response";
import { DownloadCommand } from "./download.command"; import { DownloadCommand } from "./download.command";
export class GetCommand extends DownloadCommand { export class GetCommand extends DownloadCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
@@ -114,16 +112,16 @@ export class GetCommand extends DownloadCommand {
private async getCipherView(id: string): Promise<CipherView | CipherView[]> { private async getCipherView(id: string): Promise<CipherView | CipherView[]> {
let decCipher: CipherView = null; let decCipher: CipherView = null;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (Utils.isGuid(id)) { if (Utils.isGuid(id)) {
const cipher = await this.cipherService.get(id); const cipher = await this.cipherService.get(id, activeUserId);
if (cipher != null) { if (cipher != null) {
const activeUserId = await firstValueFrom(this.activeUserId$);
decCipher = await cipher.decrypt( decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
} }
} else if (id.trim() !== "") { } else if (id.trim() !== "") {
let ciphers = await this.cipherService.getAllDecrypted(); let ciphers = await this.cipherService.getAllDecrypted(activeUserId);
ciphers = this.searchService.searchCiphersBasic(ciphers, id); ciphers = this.searchService.searchCiphersBasic(ciphers, id);
if (ciphers.length > 1) { if (ciphers.length > 1) {
return ciphers; return ciphers;
@@ -265,8 +263,10 @@ export class GetCommand extends DownloadCommand {
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(account.id), this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (!canAccessPremium) { 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 ( if (
originalCipher == null || originalCipher == null ||
originalCipher.organizationId == null || originalCipher.organizationId == null ||
@@ -352,7 +352,8 @@ export class GetCommand extends DownloadCommand {
this.accountProfileService.hasPremiumFromAnySource$(account.id), this.accountProfileService.hasPremiumFromAnySource$(account.id),
); );
if (!canAccessPremium) { 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) { if (originalCipher == null || originalCipher.organizationId == null) {
return Response.error("Premium status is required to use this feature."); 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) { private async getFolder(id: string) {
let decFolder: FolderView = null; let decFolder: FolderView = null;
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (Utils.isGuid(id)) { if (Utils.isGuid(id)) {
const folder = await this.folderService.getFromState(id, activeUserId); const folder = await this.folderService.getFromState(id, activeUserId);
if (folder != null) { if (folder != null) {
@@ -561,7 +562,7 @@ export class GetCommand extends DownloadCommand {
private async getFingerprint(id: string) { private async getFingerprint(id: string) {
let fingerprint: string[] = null; let fingerprint: string[] = null;
if (id === "me") { 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)); const publicKey = await firstValueFrom(this.keyService.userPublicKey$(activeUserId));
fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey); fingerprint = await this.keyService.getFingerprint(activeUserId, publicKey);
} else if (Utils.isGuid(id)) { } else if (Utils.isGuid(id)) {

View File

@@ -65,11 +65,14 @@ export class ListCommand {
private async listCiphers(options: Options) { private async listCiphers(options: Options) {
let ciphers: CipherView[]; let ciphers: CipherView[];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
options.trash = options.trash || false; options.trash = options.trash || false;
if (options.url != null && options.url.trim() !== "") { if (options.url != null && options.url.trim() !== "") {
ciphers = await this.cipherService.getAllDecryptedForUrl(options.url); ciphers = await this.cipherService.getAllDecryptedForUrl(options.url, activeUserId);
} else { } else {
ciphers = await this.cipherService.getAllDecrypted(); ciphers = await this.cipherService.getAllDecrypted(activeUserId);
} }
if ( if (
@@ -138,9 +141,8 @@ export class ListCommand {
} }
private async listFolders(options: Options) { private async listFolders(options: Options) {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
let folders = await this.folderService.getAllDecryptedFromState(activeUserId); let folders = await this.folderService.getAllDecryptedFromState(activeUserId);
if (options.search != null && options.search.trim() !== "") { if (options.search != null && options.search.trim() !== "") {

View File

@@ -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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Response } from "../models/response"; import { Response } from "../models/response";
export class RestoreCommand { export class RestoreCommand {
constructor(private cipherService: CipherService) {} constructor(
private cipherService: CipherService,
private accountService: AccountService,
) {}
async run(object: string, id: string): Promise<Response> { async run(object: string, id: string): Promise<Response> {
if (id != null) { if (id != null) {
@@ -19,7 +26,9 @@ export class RestoreCommand {
} }
private async restoreCipher(id: string) { 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) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -28,7 +37,7 @@ export class RestoreCommand {
} }
try { try {
await this.cipherService.restoreWithServer(id); await this.cipherService.restoreWithServer(id, activeUserId);
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);

View File

@@ -124,7 +124,10 @@ export class OssServeConfigurator {
this.serviceContainer.encryptService, this.serviceContainer.encryptService,
this.serviceContainer.organizationUserApiService, 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.shareCommand = new ShareCommand(
this.serviceContainer.cipherService, this.serviceContainer.cipherService,
this.serviceContainer.accountService, this.serviceContainer.accountService,

View File

@@ -347,7 +347,10 @@ export class VaultProgram extends BaseProgram {
} }
await this.exitIfLocked(); 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); const response = await command.run(object, id);
this.processResponse(response); this.processResponse(response);
}); });

View File

@@ -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 { 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 { SelectionReadOnlyRequest } from "@bitwarden/common/admin-console/models/request/selection-read-only.request";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherExport } from "@bitwarden/common/models/export/cipher.export"; 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"; import { FolderResponse } from "./models/folder.response";
export class CreateCommand { export class CreateCommand {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private folderService: FolderService, private folderService: FolderService,
@@ -90,7 +89,7 @@ export class CreateCommand {
} }
private async createCipher(req: CipherExport) { 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); const cipher = await this.cipherService.encrypt(CipherExport.toView(req), activeUserId);
try { try {
const newCipher = await this.cipherService.createWithServer(cipher); const newCipher = await this.cipherService.createWithServer(cipher);
@@ -132,14 +131,14 @@ export class CreateCommand {
return Response.badRequest("File name not provided."); return Response.badRequest("File name not provided.");
} }
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const itemId = options.itemId.toLowerCase(); const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId); const cipher = await this.cipherService.get(itemId, activeUserId);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
const activeUserId = await firstValueFrom(this.activeUserId$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(activeUserId), this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
); );
@@ -173,7 +172,7 @@ export class CreateCommand {
} }
private async createFolder(req: FolderExport) { 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 userKey = await this.keyService.getUserKeyWithLegacySupport(activeUserId);
const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey); const folder = await this.folderService.encrypt(FolderExport.toView(req), userKey);
try { try {

View File

@@ -1,7 +1,8 @@
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -44,7 +45,9 @@ export class DeleteCommand {
} }
private async deleteCipher(id: string, options: Options) { 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) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -59,9 +62,9 @@ export class DeleteCommand {
try { try {
if (options.permanent) { if (options.permanent) {
await this.cipherService.deleteWithServer(id); await this.cipherService.deleteWithServer(id, activeUserId);
} else { } else {
await this.cipherService.softDeleteWithServer(id); await this.cipherService.softDeleteWithServer(id, activeUserId);
} }
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
@@ -74,8 +77,10 @@ export class DeleteCommand {
return Response.badRequest("`itemid` option is required."); return Response.badRequest("`itemid` option is required.");
} }
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const itemId = options.itemId.toLowerCase(); const itemId = options.itemId.toLowerCase();
const cipher = await this.cipherService.get(itemId); const cipher = await this.cipherService.get(itemId, activeUserId);
if (cipher == null) { if (cipher == null) {
return Response.notFound(); return Response.notFound();
} }
@@ -89,16 +94,19 @@ export class DeleteCommand {
return Response.error("Attachment `" + id + "` was not found."); return Response.error("Attachment `" + id + "` was not found.");
} }
const account = await firstValueFrom(this.accountService.activeAccount$);
const canAccessPremium = await firstValueFrom( const canAccessPremium = await firstValueFrom(
this.accountProfileService.hasPremiumFromAnySource$(account.id), this.accountProfileService.hasPremiumFromAnySource$(activeUserId),
); );
if (cipher.organizationId == null && !canAccessPremium) { if (cipher.organizationId == null && !canAccessPremium) {
return Response.error("Premium status is required to use this feature."); return Response.error("Premium status is required to use this feature.");
} }
try { try {
await this.cipherService.deleteAttachmentWithServer(cipher.id, attachments[0].id); await this.cipherService.deleteAttachmentWithServer(
cipher.id,
attachments[0].id,
activeUserId,
);
return Response.success(); return Response.success();
} catch (e) { } catch (e) {
return Response.error(e); return Response.error(e);
@@ -106,9 +114,7 @@ export class DeleteCommand {
} }
private async deleteFolder(id: string) { private async deleteFolder(id: string) {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const folder = await this.folderService.getFromState(id, activeUserId); const folder = await this.folderService.getFromState(id, activeUserId);
if (folder == null) { if (folder == null) {
return Response.notFound(); return Response.notFound();

View File

@@ -4,6 +4,7 @@ import {
EMPTY, EMPTY,
Subject, Subject,
distinctUntilChanged, distinctUntilChanged,
filter,
firstValueFrom, firstValueFrom,
map, map,
mergeMap, mergeMap,
@@ -12,6 +13,7 @@ import {
} from "rxjs"; } from "rxjs";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service"; import { UriMatchStrategy } from "@bitwarden/common/models/domain/domain-service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.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 { getCredentialsForAutofill } from "@bitwarden/common/platform/services/fido2/fido2-autofill-utils";
import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils"; import { Fido2Utils } from "@bitwarden/common/platform/services/fido2/fido2-utils";
import { guidToRawFormat } from "@bitwarden/common/platform/services/fido2/guid-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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -60,7 +63,11 @@ export class DesktopAutofillService implements OnDestroy {
return EMPTY; 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 // TODO: This will unset all the autofill credentials on the OS
// when the account locks. We should instead explicilty clear the credentials // 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 // 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. // get it from the cipher. For that we use the recordIdentifier, which is the cipherId.
if (request.recordIdentifier && request.credentialId.length === 0) { 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) { if (!cipher) {
this.logService.error("listenPasskeyAssertion error", "Cipher not found"); this.logService.error("listenPasskeyAssertion error", "Cipher not found");
callback(new Error("Cipher not found"), null); callback(new Error("Cipher not found"), null);
return; return;
} }
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const decrypted = await cipher.decrypt( const decrypted = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );

View File

@@ -29,6 +29,7 @@ import { ConfigService } from "@bitwarden/common/platform/abstractions/config/co
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CommandDefinition, MessageListener } from "@bitwarden/common/platform/messaging"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
@@ -92,14 +93,14 @@ export class SshAgentService implements OnDestroy {
}), }),
filter(({ enabled }) => enabled), filter(({ enabled }) => enabled),
map(({ message }) => message), map(({ message }) => message),
withLatestFrom(this.authService.activeAccountStatus$), withLatestFrom(this.authService.activeAccountStatus$, this.accountService.activeAccount$),
// This switchMap handles unlocking the vault if it is locked: // 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 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 not unlocked within the timeout, we will abort the flow.
// - If the vault is unlocked, we will continue with 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, // 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. // and will cancel the previous request if a new one is received.
switchMap(([message, status]) => { switchMap(([message, status, account]) => {
if (status !== AuthenticationStatus.Unlocked) { if (status !== AuthenticationStatus.Unlocked) {
ipc.platform.focusWindow(); ipc.platform.focusWindow();
this.toastService.showToast({ 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. // This switchMap handles fetching the ciphers from the vault.
switchMap((message) => switchMap(([message, userId]: [Record<string, unknown>, UserId]) =>
from(this.cipherService.getAllDecrypted()).pipe( from(this.cipherService.getAllDecrypted(userId)).pipe(
map((ciphers) => [message, ciphers] as const), map((ciphers) => [message, ciphers] as const),
), ),
), ),
@@ -245,7 +246,7 @@ export class SshAgentService implements OnDestroy {
return; return;
} }
const ciphers = await this.cipherService.getAllDecrypted(); const ciphers = await this.cipherService.getAllDecrypted(activeAccount.id);
if (ciphers == null) { if (ciphers == null) {
await ipc.platform.sshAgent.lock(); await ipc.platform.sshAgent.lock();
return; return;

View File

@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service"; import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status"; 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 { MessagingService } from "@bitwarden/common/platform/abstractions/messaging.service";
import { UserId } from "@bitwarden/common/types/guid"; import { UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -65,9 +66,7 @@ export class EncryptedMessageHandlerService {
} }
private async checkUserStatus(userId: string): Promise<string> { private async checkUserStatus(userId: string): Promise<string> {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (userId !== activeUserId) { if (userId !== activeUserId) {
return "not-active-user"; return "not-active-user";
@@ -83,9 +82,7 @@ export class EncryptedMessageHandlerService {
private async statusCommandHandler(): Promise<AccountStatusResponse[]> { private async statusCommandHandler(): Promise<AccountStatusResponse[]> {
const accounts = await firstValueFrom(this.accountService.accounts$); const accounts = await firstValueFrom(this.accountService.accounts$);
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (!accounts || !Object.keys(accounts)) { if (!accounts || !Object.keys(accounts)) {
return []; return [];
@@ -114,16 +111,14 @@ export class EncryptedMessageHandlerService {
} }
const ciphersResponse: CipherResponse[] = []; const ciphersResponse: CipherResponse[] = [];
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const authStatus = await this.authService.getAuthStatus(activeUserId); const authStatus = await this.authService.getAuthStatus(activeUserId);
if (authStatus !== AuthenticationStatus.Unlocked) { if (authStatus !== AuthenticationStatus.Unlocked) {
return { error: "locked" }; 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.sort((a, b) => this.cipherService.sortCiphersByLastUsedThenName(a, b));
ciphers.forEach((c) => { ciphers.forEach((c) => {
@@ -166,9 +161,7 @@ export class EncryptedMessageHandlerService {
cipherView.login.uris[0].uri = credentialCreatePayload.uri; cipherView.login.uris[0].uri = credentialCreatePayload.uri;
try { try {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const encrypted = await this.cipherService.encrypt(cipherView, activeUserId); const encrypted = await this.cipherService.encrypt(cipherView, activeUserId);
await this.cipherService.createWithServer(encrypted); await this.cipherService.createWithServer(encrypted);
@@ -200,13 +193,16 @@ export class EncryptedMessageHandlerService {
} }
try { 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) { if (cipher === null) {
return { status: "failure" }; return { status: "failure" };
} }
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipherView = await cipher.decrypt( const cipherView = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );

View File

@@ -5,6 +5,7 @@ import { distinctUntilChanged } from "rxjs";
import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component"; import { VaultItemsComponent as BaseVaultItemsComponent } from "@bitwarden/angular/vault/components/vault-items.component";
import { SearchService } from "@bitwarden/common/abstractions/search.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -20,8 +21,9 @@ export class VaultItemsComponent extends BaseVaultItemsComponent {
searchService: SearchService, searchService: SearchService,
searchBarService: SearchBarService, searchBarService: SearchBarService,
cipherService: CipherService, cipherService: CipherService,
accountService: AccountService,
) { ) {
super(searchService, cipherService); super(searchService, cipherService, accountService);
// eslint-disable-next-line rxjs-angular/prefer-takeuntil // eslint-disable-next-line rxjs-angular/prefer-takeuntil
searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => { searchBarService.searchText$.pipe(distinctUntilChanged()).subscribe((searchText) => {

View File

@@ -10,7 +10,7 @@ import {
ViewContainerRef, ViewContainerRef,
} from "@angular/core"; } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router"; 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 { filter, first, map, take } from "rxjs/operators";
import { ModalRef } from "@bitwarden/angular/components/modal/modal.ref"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { BroadcasterService } from "@bitwarden/common/platform/abstractions/broadcaster.service"; 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 activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const activeAccount = await firstValueFrom(this.accountService.activeAccount$);
// Combine with the activeAccount$ to ensure we only show the dialog for the current account from ngOnInit. this.cipherService
// The account switching process updates the cipherService before Vault is destroyed and would cause duplicate emissions .failedToDecryptCiphers$(activeUserId)
combineLatest([this.accountService.activeAccount$, this.cipherService.failedToDecryptCiphers$])
.pipe( .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), filter((ciphers) => ciphers.length > 0),
take(1), take(1),
takeUntil(this.componentIsDestroyed$), takeUntil(this.componentIsDestroyed$),

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { AuditService } from "@bitwarden/common/abstractions/audit.service"; import { AuditService } from "@bitwarden/common/abstractions/audit.service";
@@ -11,6 +11,7 @@ import {
OrganizationService, OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -59,15 +60,13 @@ export class ExposedPasswordsReportComponent
this.isAdminConsoleActive = true; this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom( const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom( this.organization = await firstValueFrom(
this.organizationService this.organizationService
.organizations$(userId) .organizations$(userId)
.pipe(getOrganizationById(params.organizationId)), .pipe(getOrganizationById(params.organizationId)),
); );
this.manageableCiphers = await this.cipherService.getAll(); this.manageableCiphers = await this.cipherService.getAll(userId);
}); });
} }

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { import {
@@ -10,6 +10,7 @@ import {
OrganizationService, OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
@@ -56,15 +57,13 @@ export class ReusedPasswordsReportComponent
this.isAdminConsoleActive = true; this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom( const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom( this.organization = await firstValueFrom(
this.organizationService this.organizationService
.organizations$(userId) .organizations$(userId)
.pipe(getOrganizationById(params.organizationId)), .pipe(getOrganizationById(params.organizationId)),
); );
this.manageableCiphers = await this.cipherService.getAll(); this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit(); await super.ngOnInit();
}); });
} }

View File

@@ -2,7 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { ActivatedRoute } from "@angular/router"; import { ActivatedRoute } from "@angular/router";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ModalService } from "@bitwarden/angular/services/modal.service"; import { ModalService } from "@bitwarden/angular/services/modal.service";
import { import {
@@ -10,6 +10,7 @@ import {
OrganizationService, OrganizationService,
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength"; import { PasswordStrengthServiceAbstraction } from "@bitwarden/common/tools/password-strength";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -59,15 +60,14 @@ export class WeakPasswordsReportComponent
this.isAdminConsoleActive = true; this.isAdminConsoleActive = true;
// eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe // eslint-disable-next-line rxjs-angular/prefer-takeuntil, rxjs/no-async-subscribe
this.route.parent.parent.params.subscribe(async (params) => { this.route.parent.parent.params.subscribe(async (params) => {
const userId = await firstValueFrom( const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.organization = await firstValueFrom( this.organization = await firstValueFrom(
this.organizationService this.organizationService
.organizations$(userId) .organizations$(userId)
.pipe(getOrganizationById(params.organizationId)), .pipe(getOrganizationById(params.organizationId)),
); );
this.manageableCiphers = await this.cipherService.getAll(); this.manageableCiphers = await this.cipherService.getAll(userId);
await super.ngOnInit(); await super.ngOnInit();
}); });
} }

View File

@@ -88,7 +88,9 @@ export class ChangePasswordComponent
async rotateUserKeyClicked() { async rotateUserKeyClicked() {
if (this.rotateUserKey) { 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; let hasOldAttachments = false;
if (ciphers != null) { if (ciphers != null) {
for (let i = 0; i < ciphers.length; i++) { for (let i = 0; i < ciphers.length; i++) {

View File

@@ -1,12 +1,13 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Directive, ViewChild, ViewContainerRef, OnDestroy } from "@angular/core"; 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 { ModalService } from "@bitwarden/angular/services/modal.service";
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { OrganizationId } from "@bitwarden/common/types/guid"; import { OrganizationId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -51,8 +52,10 @@ export class CipherReportComponent implements OnDestroy {
private syncService: SyncService, private syncService: SyncService,
) { ) {
this.organizations$ = this.accountService.activeAccount$.pipe( 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$.pipe(takeUntil(this.destroyed$)).subscribe((orgs) => {
this.organizations = orgs; this.organizations = orgs;
}); });
@@ -182,7 +185,8 @@ export class CipherReportComponent implements OnDestroy {
} }
protected async getAllCiphers(): Promise<CipherView[]> { protected async getAllCiphers(): Promise<CipherView[]> {
return await this.cipherService.getAllDecrypted(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return await this.cipherService.getAllDecrypted(activeUserId);
} }
protected filterCiphersByOrg(ciphersList: CipherView[]) { protected filterCiphersByOrg(ciphersList: CipherView[]) {

View File

@@ -25,15 +25,14 @@ describe("ExposedPasswordsReportComponent", () => {
let auditService: MockProxy<AuditService>; let auditService: MockProxy<AuditService>;
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>; let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId; const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => { beforeEach(() => {
syncServiceMock = mock<SyncService>(); syncServiceMock = mock<SyncService>();
auditService = mock<AuditService>(); auditService = mock<AuditService>();
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([])); 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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -24,14 +24,13 @@ describe("InactiveTwoFactorReportComponent", () => {
let fixture: ComponentFixture<InactiveTwoFactorReportComponent>; let fixture: ComponentFixture<InactiveTwoFactorReportComponent>;
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>; let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId; const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => { beforeEach(() => {
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([])); organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>(); syncServiceMock = mock<SyncService>();
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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -23,15 +23,13 @@ describe("ReusedPasswordsReportComponent", () => {
let fixture: ComponentFixture<ReusedPasswordsReportComponent>; let fixture: ComponentFixture<ReusedPasswordsReportComponent>;
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>; let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId; const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => { beforeEach(() => {
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([])); organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>(); syncServiceMock = mock<SyncService>();
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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -25,15 +25,14 @@ describe("UnsecuredWebsitesReportComponent", () => {
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>; let syncServiceMock: MockProxy<SyncService>;
let collectionService: MockProxy<CollectionService>; let collectionService: MockProxy<CollectionService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId; const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => { beforeEach(() => {
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([])); organizationService.organizations$.mockReturnValue(of([]));
syncServiceMock = mock<SyncService>(); syncServiceMock = mock<SyncService>();
collectionService = mock<CollectionService>(); collectionService = mock<CollectionService>();
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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -25,15 +25,14 @@ describe("WeakPasswordsReportComponent", () => {
let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>; let passwordStrengthService: MockProxy<PasswordStrengthServiceAbstraction>;
let organizationService: MockProxy<OrganizationService>; let organizationService: MockProxy<OrganizationService>;
let syncServiceMock: MockProxy<SyncService>; let syncServiceMock: MockProxy<SyncService>;
let accountService: FakeAccountService;
const userId = Utils.newGuid() as UserId; const userId = Utils.newGuid() as UserId;
const accountService: FakeAccountService = mockAccountServiceWith(userId);
beforeEach(() => { beforeEach(() => {
syncServiceMock = mock<SyncService>(); syncServiceMock = mock<SyncService>();
passwordStrengthService = mock<PasswordStrengthServiceAbstraction>(); passwordStrengthService = mock<PasswordStrengthServiceAbstraction>();
organizationService = mock<OrganizationService>(); organizationService = mock<OrganizationService>();
organizationService.organizations$.mockReturnValue(of([])); 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. // 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 // eslint-disable-next-line @typescript-eslint/no-floating-promises
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -12,6 +12,7 @@ import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; 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.mode = "edit";
this.formConfig.initialValues = null; this.formConfig.initialValues = null;
} }
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
let cipher = await this.cipherService.get(cipherView.id); 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) // 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)) { 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.Removed ||
result.action === AttachmentDialogResult.Uploaded result.action === AttachmentDialogResult.Uploaded
) { ) {
const updatedCipher = await this.cipherService.get(this.formConfig.originalCipher?.id);
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)), this.accountService.activeAccount$.pipe(map((a) => a?.id)),
); );
const updatedCipher = await this.cipherService.get(
this.formConfig.originalCipher?.id,
activeUserId,
);
const updatedCipherView = await updatedCipher.decrypt( const updatedCipherView = await updatedCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(updatedCipher, activeUserId),
@@ -490,9 +494,7 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
if (config.originalCipher == null) { if (config.originalCipher == null) {
return; return;
} }
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
return await config.originalCipher.decrypt( return await config.originalCipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(config.originalCipher, activeUserId),
); );
@@ -574,10 +576,14 @@ export class VaultItemDialogComponent implements OnInit, OnDestroy {
// - The cipher is unassigned // - The cipher is unassigned
const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned; const asAdmin = this.organization?.canEditAllCiphers || cipherIsUnassigned;
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
if (this.cipher.isDeleted) { if (this.cipher.isDeleted) {
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); await this.cipherService.deleteWithServer(this.cipher.id, activeUserId, asAdmin);
} else { } else {
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); await this.cipherService.softDeleteWithServer(this.cipher.id, activeUserId, asAdmin);
} }
} }

View File

@@ -2,10 +2,13 @@
// @ts-strict-ignore // @ts-strict-ignore
import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog"; import { DIALOG_DATA, DialogConfig, DialogRef } from "@angular/cdk/dialog";
import { Component, Inject } from "@angular/core"; import { Component, Inject } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -61,6 +64,7 @@ export class BulkDeleteDialogComponent {
private apiService: ApiService, private apiService: ApiService,
private collectionService: CollectionService, private collectionService: CollectionService,
private toastService: ToastService, private toastService: ToastService,
private accountService: AccountService,
) { ) {
this.cipherIds = params.cipherIds ?? []; this.cipherIds = params.cipherIds ?? [];
this.permanent = params.permanent; this.permanent = params.permanent;
@@ -115,10 +119,12 @@ export class BulkDeleteDialogComponent {
private async deleteCiphers(): Promise<any> { private async deleteCiphers(): Promise<any> {
const asAdmin = this.organization?.canEditAllCiphers; const asAdmin = this.organization?.canEditAllCiphers;
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (this.permanent) { if (this.permanent) {
await this.cipherService.deleteManyWithServer(this.cipherIds, asAdmin); await this.cipherService.deleteManyWithServer(this.cipherIds, activeUserId, asAdmin);
} else { } else {
await this.cipherService.softDeleteManyWithServer(this.cipherIds, asAdmin); await this.cipherService.softDeleteManyWithServer(this.cipherIds, activeUserId, asAdmin);
} }
} }

View File

@@ -3,9 +3,10 @@
import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog"; import { DialogConfig, DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
import { Component, Inject, OnInit } from "@angular/core"; import { Component, Inject, OnInit } from "@angular/core";
import { FormBuilder, Validators } from "@angular/forms"; 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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -48,8 +49,6 @@ export class BulkMoveDialogComponent implements OnInit {
}); });
folders$: Observable<FolderView[]>; folders$: Observable<FolderView[]>;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
@Inject(DIALOG_DATA) params: BulkMoveDialogParams, @Inject(DIALOG_DATA) params: BulkMoveDialogParams,
private dialogRef: DialogRef<BulkMoveDialogResult>, private dialogRef: DialogRef<BulkMoveDialogResult>,
@@ -65,7 +64,7 @@ export class BulkMoveDialogComponent implements OnInit {
} }
async ngOnInit() { async ngOnInit() {
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.folders$ = this.folderService.folderViews$(activeUserId); this.folders$ = this.folderService.folderViews$(activeUserId);
this.formGroup.patchValue({ this.formGroup.patchValue({
folderId: (await firstValueFrom(this.folders$))[0].id, folderId: (await firstValueFrom(this.folders$))[0].id,
@@ -81,7 +80,12 @@ export class BulkMoveDialogComponent implements OnInit {
return; 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({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,

View File

@@ -71,7 +71,7 @@ describe("vault filter service", () => {
policyService.policyAppliesToActiveUser$ policyService.policyAppliesToActiveUser$
.calledWith(PolicyType.SingleOrg) .calledWith(PolicyType.SingleOrg)
.mockReturnValue(singleOrgPolicy); .mockReturnValue(singleOrgPolicy);
cipherService.cipherViews$ = cipherViews; cipherService.cipherViews$.mockReturnValue(cipherViews);
vaultFilterService = new VaultFilterService( vaultFilterService = new VaultFilterService(
organizationService, organizationService,

View File

@@ -68,7 +68,7 @@ export class VaultFilterService implements VaultFilterServiceAbstraction {
switchMap((userId) => switchMap((userId) =>
combineLatest([ combineLatest([
this.folderService.folderViews$(userId), this.folderService.folderViews$(userId),
this.cipherService.cipherViews$, this.cipherService.cipherViews$(userId),
this._organizationFilter, this._organizationFilter,
]), ]),
), ),

View File

@@ -44,6 +44,7 @@ import {
} from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service"; import { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
@@ -166,7 +167,6 @@ export class VaultComponent implements OnInit, OnDestroy {
protected selectedCollection: TreeNode<CollectionView> | undefined; protected selectedCollection: TreeNode<CollectionView> | undefined;
protected canCreateCollections = false; protected canCreateCollections = false;
protected currentSearchText$: Observable<string>; protected currentSearchText$: Observable<string>;
private activeUserId: UserId;
private searchText$ = new Subject<string>(); private searchText$ = new Subject<string>();
private refresh$ = new BehaviorSubject<void>(null); private refresh$ = new BehaviorSubject<void>(null);
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
@@ -271,9 +271,7 @@ export class VaultComponent implements OnInit, OnDestroy {
: "trashCleanupWarning", : "trashCleanupWarning",
); );
this.activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const firstSetup$ = this.route.queryParams.pipe( const firstSetup$ = this.route.queryParams.pipe(
first(), first(),
@@ -337,13 +335,15 @@ export class VaultComponent implements OnInit, OnDestroy {
this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search)); this.currentSearchText$ = this.route.queryParams.pipe(map((queryParams) => queryParams.search));
const ciphers$ = combineLatest([ const ciphers$ = combineLatest([
this.cipherService.cipherViews$.pipe(filter((c) => c !== null)), this.cipherService.cipherViews$(activeUserId).pipe(filter((c) => c !== null)),
filter$, filter$,
this.currentSearchText$, this.currentSearchText$,
]).pipe( ]).pipe(
filter(([ciphers, filter]) => ciphers != undefined && filter != undefined), filter(([ciphers, filter]) => ciphers != undefined && filter != undefined),
concatMap(async ([ciphers, filter, searchText]) => { concatMap(async ([ciphers, filter, searchText]) => {
const failedCiphers = await firstValueFrom(this.cipherService.failedToDecryptCiphers$); const failedCiphers = await firstValueFrom(
this.cipherService.failedToDecryptCiphers$(activeUserId),
);
const filterFunction = createFilterFunction(filter); const filterFunction = createFilterFunction(filter);
// Append any failed to decrypt ciphers to the top of the cipher list // Append any failed to decrypt ciphers to the top of the cipher list
const allCiphers = [...failedCiphers, ...ciphers]; const allCiphers = [...failedCiphers, ...ciphers];
@@ -416,7 +416,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(async (params) => { switchMap(async (params) => {
const cipherId = getCipherIdFromParams(params); const cipherId = getCipherIdFromParams(params);
if (cipherId) { if (cipherId) {
if (await this.cipherService.get(cipherId)) { if (await this.cipherService.get(cipherId, activeUserId)) {
let action = params.action; let action = params.action;
// Default to "view" // Default to "view"
if (action == null) { if (action == null) {
@@ -459,7 +459,7 @@ export class VaultComponent implements OnInit, OnDestroy {
firstSetup$ firstSetup$
.pipe( .pipe(
switchMap(() => this.cipherService.failedToDecryptCiphers$), switchMap(() => this.cipherService.failedToDecryptCiphers$(activeUserId)),
map((ciphers) => ciphers.filter((c) => !c.isDeleted)), map((ciphers) => ciphers.filter((c) => !c.isDeleted)),
filter((ciphers) => ciphers.length > 0), filter((ciphers) => ciphers.length > 0),
take(1), take(1),
@@ -480,7 +480,7 @@ export class VaultComponent implements OnInit, OnDestroy {
switchMap(() => switchMap(() =>
combineLatest([ combineLatest([
filter$, filter$,
this.billingAccountProfileStateService.hasPremiumFromAnySource$(this.activeUserId), this.billingAccountProfileStateService.hasPremiumFromAnySource$(activeUserId),
allCollections$, allCollections$,
this.organizations$, this.organizations$,
ciphers$, ciphers$,
@@ -732,7 +732,8 @@ export class VaultComponent implements OnInit, OnDestroy {
* @returns * @returns
*/ */
async editCipherId(id: string, cloneMode?: boolean) { 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 ( if (
cipher && cipher &&
@@ -768,7 +769,8 @@ export class VaultComponent implements OnInit, OnDestroy {
* @returns Promise<void> * @returns Promise<void>
*/ */
async viewCipherById(id: string) { 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 // If cipher exists (cipher is null when new) and MP reprompt
// is on for this cipher, then show password reprompt. // is on for this cipher, then show password reprompt.
if ( if (
@@ -959,7 +961,8 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
try { 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({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
@@ -1041,7 +1044,8 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
try { 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({ this.toastService.showToast({
variant: "success", 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 return permanent
? this.cipherService.deleteWithServer(id) ? this.cipherService.deleteWithServer(id, userId)
: this.cipherService.softDeleteWithServer(id); : this.cipherService.softDeleteWithServer(id, userId);
} }
protected async repromptCipher(ciphers: CipherView[]) { protected async repromptCipher(ciphers: CipherView[]) {

View File

@@ -165,10 +165,11 @@ export class ViewComponent implements OnInit {
*/ */
protected async deleteCipher(): Promise<void> { protected async deleteCipher(): Promise<void> {
const asAdmin = this.organization?.canEditAllCiphers; const asAdmin = this.organization?.canEditAllCiphers;
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (this.cipher.isDeleted) { if (this.cipher.isDeleted) {
await this.cipherService.deleteWithServer(this.cipher.id, asAdmin); await this.cipherService.deleteWithServer(this.cipher.id, userId, asAdmin);
} else { } else {
await this.cipherService.softDeleteWithServer(this.cipher.id, asAdmin); await this.cipherService.softDeleteWithServer(this.cipher.id, userId, asAdmin);
} }
} }

View File

@@ -2,6 +2,7 @@
// @ts-strict-ignore // @ts-strict-ignore
import { DatePipe } from "@angular/common"; import { DatePipe } from "@angular/common";
import { Component } from "@angular/core"; import { Component } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { CollectionService } from "@bitwarden/admin-console/common"; import { CollectionService } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service"; import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
@@ -98,8 +100,9 @@ export class AddEditComponent extends BaseAddEditComponent {
protected async loadCipher() { protected async loadCipher() {
this.isAdminConsoleAction = true; 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 // 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) { if (!this.organization.canEditAllCiphers && firstCipherCheck != null) {
return firstCipherCheck; return firstCipherCheck;
@@ -123,7 +126,8 @@ export class AddEditComponent extends BaseAddEditComponent {
protected async deleteCipher() { protected async deleteCipher() {
if (!this.organization.canEditAllCiphers) { if (!this.organization.canEditAllCiphers) {
return super.deleteCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return super.deleteCipher(activeUserId);
} }
return this.cipher.isDeleted return this.cipher.isDeleted
? this.apiService.deleteCipherAdmin(this.cipherId) ? this.apiService.deleteCipherAdmin(this.cipherId)

View File

@@ -1,10 +1,12 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Component, OnInit } from "@angular/core"; import { Component, OnInit } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.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() { protected async loadCipher() {
if (!this.organization.canEditAllCiphers) { 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); const response = await this.apiService.getCipherAdmin(this.cipherId);
return new Cipher(new CipherData(response)); 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) { if (!this.organization.canEditAllCiphers) {
return super.deleteCipherAttachment(attachmentId); return super.deleteCipherAttachment(attachmentId, userId);
} }
return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId); return this.apiService.deleteCipherAttachmentAdmin(this.cipherId, attachmentId);
} }

View File

@@ -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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -26,6 +27,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
isMember: true, isMember: true,
enabled: true, enabled: true,
status: OrganizationUserStatusType.Confirmed, status: OrganizationUserStatusType.Confirmed,
userId: "UserId",
}; };
const testOrg2 = { const testOrg2 = {
id: "333-999-888", id: "333-999-888",
@@ -34,6 +36,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
isMember: true, isMember: true,
enabled: true, enabled: true,
status: OrganizationUserStatusType.Confirmed, status: OrganizationUserStatusType.Confirmed,
userId: "UserId",
}; };
const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true); const policyAppliesToActiveUser$ = new BehaviorSubject<boolean>(true);
const collection = { const collection = {
@@ -80,17 +83,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
}, },
{ provide: ApiService, useValue: { getCipherAdmin } }, { provide: ApiService, useValue: { getCipherAdmin } },
{ provide: CipherService, useValue: { get: getCipher } }, { provide: CipherService, useValue: { get: getCipher } },
{ { provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
provide: AccountService,
useValue: {
activeAccount$: new BehaviorSubject<Account>({
id: "123-456-789" as UserId,
email: "test@email.com",
emailVerified: true,
name: "Test User",
}),
},
},
], ],
}); });
adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService); adminConsoleConfigService = TestBed.inject(AdminConsoleCipherFormConfigService);
@@ -207,7 +200,7 @@ describe("AdminConsoleCipherFormConfigService", () => {
await adminConsoleConfigService.buildConfig("edit", cipherId); await adminConsoleConfigService.buildConfig("edit", cipherId);
expect(getCipherAdmin).not.toHaveBeenCalled(); expect(getCipherAdmin).not.toHaveBeenCalled();
expect(getCipher).toHaveBeenCalledWith(cipherId); expect(getCipher).toHaveBeenCalledWith(cipherId, "UserId");
}); });
}); });
}); });

View File

@@ -10,7 +10,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data"; import { CipherData } from "@bitwarden/common/vault/models/data/cipher.data";
@@ -100,7 +100,7 @@ export class AdminConsoleCipherFormConfigService implements CipherFormConfigServ
return null; 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) // 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) { if (organization.canEditAllCiphers || localCipher == null) {

View File

@@ -40,6 +40,7 @@ import { OrganizationApiServiceAbstraction } from "@bitwarden/common/admin-conso
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { OrganizationBillingServiceAbstraction } from "@bitwarden/common/billing/abstractions";
import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction"; import { BillingApiServiceAbstraction } from "@bitwarden/common/billing/abstractions/billing-api.service.abstraction";
import { EventType } from "@bitwarden/common/enums"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { Utils } from "@bitwarden/common/platform/misc/utils"; import { Utils } from "@bitwarden/common/platform/misc/utils";
import { SyncService } from "@bitwarden/common/platform/sync"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
import { CipherType } from "@bitwarden/common/vault/enums"; import { CipherType } from "@bitwarden/common/vault/enums";
@@ -952,8 +953,9 @@ export class VaultComponent implements OnInit, OnDestroy {
// Allow restore of an Unassigned Item // Allow restore of an Unassigned Item
try { try {
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const asAdmin = this.organization?.canEditAnyCollection || c.isUnassigned; 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({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
@@ -1044,7 +1046,8 @@ export class VaultComponent implements OnInit, OnDestroy {
} }
try { 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({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, 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; const asAdmin = this.organization?.canEditAllCiphers || isUnassigned;
return permanent return permanent
? this.cipherService.deleteWithServer(id, asAdmin) ? this.cipherService.deleteWithServer(id, userId, asAdmin)
: this.cipherService.softDeleteWithServer(id, asAdmin); : this.cipherService.softDeleteWithServer(id, userId, asAdmin);
} }
protected async repromptCipher(ciphers: CipherView[]) { protected async repromptCipher(ciphers: CipherView[]) {

View File

@@ -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 { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -45,11 +47,9 @@ export class CollectionsComponent implements OnInit {
} }
async load() { 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(); this.collectionIds = this.loadCipherCollections();
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.cipher = await this.cipherDomain.decrypt( this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
); );
@@ -95,7 +95,8 @@ export class CollectionsComponent implements OnInit {
} }
this.cipherDomain.collectionIds = selectedCollectionIds; this.cipherDomain.collectionIds = selectedCollectionIds;
try { try {
this.formPromise = this.saveCollections(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.formPromise = this.saveCollections(activeUserId);
await this.formPromise; await this.formPromise;
this.onSavedCollections.emit(); this.onSavedCollections.emit();
this.toastService.showToast({ this.toastService.showToast({
@@ -114,8 +115,8 @@ export class CollectionsComponent implements OnInit {
} }
} }
protected loadCipher() { protected loadCipher(userId: UserId) {
return this.cipherService.get(this.cipherId); return this.cipherService.get(this.cipherId, userId);
} }
protected loadCipherCollections() { protected loadCipherCollections() {
@@ -129,7 +130,7 @@ export class CollectionsComponent implements OnInit {
); );
} }
protected saveCollections() { protected saveCollections(userId: UserId) {
return this.cipherService.saveCollectionsWithServer(this.cipherDomain); return this.cipherService.saveCollectionsWithServer(this.cipherDomain, userId);
} }
} }

View File

@@ -8,6 +8,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.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(getUserId));
const activeUserId = await firstValueFrom( const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.cipher = await cipherDomain.decrypt( this.cipher = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
); );
@@ -104,10 +103,8 @@ export class ShareComponent implements OnInit, OnDestroy {
return; return;
} }
const cipherDomain = await this.cipherService.get(this.cipherId); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const activeUserId = await firstValueFrom( const cipherDomain = await this.cipherService.get(this.cipherId, activeUserId);
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipherView = await cipherDomain.decrypt( const cipherView = await cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipherDomain, activeUserId),
); );

View File

@@ -12,6 +12,7 @@ import { PolicyService } from "@bitwarden/common/admin-console/abstractions/poli
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { normalizeExpiryYearFormat } from "@bitwarden/common/autofill/utils";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum"; import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
@@ -101,8 +102,6 @@ export class AddEditComponent implements OnInit, OnDestroy {
private personalOwnershipPolicyAppliesToActiveUser: boolean; private personalOwnershipPolicyAppliesToActiveUser: boolean;
private previousCipherId: string; private previousCipherId: string;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
get fido2CredentialCreationDateValue(): string { get fido2CredentialCreationDateValue(): string {
const dateCreated = this.i18nService.t("dateCreated"); const dateCreated = this.i18nService.t("dateCreated");
const creationDate = this.datePipe.transform( const creationDate = this.datePipe.transform(
@@ -263,12 +262,13 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.title = this.i18nService.t("addItem"); 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.cipher == null) {
if (this.editMode) { if (this.editMode) {
const cipher = await this.loadCipher(); const cipher = await this.loadCipher(activeUserId);
this.cipher = await cipher.decrypt( this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
@@ -420,9 +420,7 @@ export class AddEditComponent implements OnInit, OnDestroy {
this.cipher.id = null; this.cipher.id = null;
} }
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipher = await this.encryptCipher(activeUserId); const cipher = await this.encryptCipher(activeUserId);
try { try {
this.formPromise = this.saveCipher(cipher); this.formPromise = this.saveCipher(cipher);
@@ -516,7 +514,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
try { try {
this.deletePromise = this.deleteCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.deletePromise = this.deleteCipher(activeUserId);
await this.deletePromise; await this.deletePromise;
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
@@ -542,7 +541,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
} }
try { try {
this.restorePromise = this.restoreCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.restorePromise = this.restoreCipher(activeUserId);
await this.restorePromise; await this.restorePromise;
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
@@ -725,8 +725,8 @@ export class AddEditComponent implements OnInit, OnDestroy {
return allCollections.filter((c) => !c.readOnly); return allCollections.filter((c) => !c.readOnly);
} }
protected loadCipher() { protected loadCipher(userId: UserId) {
return this.cipherService.get(this.cipherId); return this.cipherService.get(this.cipherId, userId);
} }
protected encryptCipher(userId: UserId) { protected encryptCipher(userId: UserId) {
@@ -746,14 +746,14 @@ export class AddEditComponent implements OnInit, OnDestroy {
: this.cipherService.updateWithServer(cipher, orgAdmin); : this.cipherService.updateWithServer(cipher, orgAdmin);
} }
protected deleteCipher() { protected deleteCipher(userId: UserId) {
return this.cipher.isDeleted return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id, this.asAdmin) ? this.cipherService.deleteWithServer(this.cipher.id, userId, this.asAdmin)
: this.cipherService.softDeleteWithServer(this.cipher.id, this.asAdmin); : this.cipherService.softDeleteWithServer(this.cipher.id, userId, this.asAdmin);
} }
protected restoreCipher() { protected restoreCipher(userId: UserId) {
return this.cipherService.restoreWithServer(this.cipher.id, this.asAdmin); 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; return this.ownershipOptions[0].value;
} }
async loadAddEditCipherInfo(): Promise<boolean> { async loadAddEditCipherInfo(userId: UserId): Promise<boolean> {
const addEditCipherInfo: any = await firstValueFrom(this.cipherService.addEditCipherInfo$); const addEditCipherInfo: any = await firstValueFrom(
this.cipherService.addEditCipherInfo$(userId),
);
const loadedSavedInfo = addEditCipherInfo != null; const loadedSavedInfo = addEditCipherInfo != null;
if (loadedSavedInfo) { 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; return loadedSavedInfo;
} }

View File

@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; 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 { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response"; import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
@@ -84,9 +85,7 @@ export class AttachmentsComponent implements OnInit {
} }
try { try {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.formPromise = this.saveCipherAttachment(files[0], activeUserId); this.formPromise = this.saveCipherAttachment(files[0], activeUserId);
this.cipherDomain = await this.formPromise; this.cipherDomain = await this.formPromise;
this.cipher = await this.cipherDomain.decrypt( this.cipher = await this.cipherDomain.decrypt(
@@ -125,12 +124,11 @@ export class AttachmentsComponent implements OnInit {
} }
try { 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 updatedCipher = await this.deletePromises[attachment.id];
const activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const cipher = new Cipher(updatedCipher); const cipher = new Cipher(updatedCipher);
this.cipher = await cipher.decrypt( this.cipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
@@ -228,10 +226,8 @@ export class AttachmentsComponent implements OnInit {
} }
protected async init() { protected async init() {
this.cipherDomain = await this.loadCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const activeUserId = await firstValueFrom( this.cipherDomain = await this.loadCipher(activeUserId);
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.cipher = await this.cipherDomain.decrypt( this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId),
); );
@@ -287,7 +283,7 @@ export class AttachmentsComponent implements OnInit {
: await this.keyService.getOrgKey(this.cipher.organizationId); : await this.keyService.getOrgKey(this.cipher.organizationId);
const decBuf = await this.encryptService.decryptToBytes(encBuf, key); const decBuf = await this.encryptService.decryptToBytes(encBuf, key);
const activeUserId = await firstValueFrom( 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 = await this.cipherService.saveAttachmentRawWithServer(
this.cipherDomain, this.cipherDomain,
@@ -301,7 +297,10 @@ export class AttachmentsComponent implements OnInit {
); );
// 3. Delete old // 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]; await this.deletePromises[attachment.id];
const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id); const foundAttachment = this.cipher.attachments.filter((a2) => a2.id === attachment.id);
if (foundAttachment.length > 0) { if (foundAttachment.length > 0) {
@@ -335,16 +334,16 @@ export class AttachmentsComponent implements OnInit {
} }
} }
protected loadCipher() { protected loadCipher(userId: UserId) {
return this.cipherService.get(this.cipherId); return this.cipherService.get(this.cipherId, userId);
} }
protected saveCipherAttachment(file: File, userId: UserId) { protected saveCipherAttachment(file: File, userId: UserId) {
return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId); return this.cipherService.saveAttachmentWithServer(this.cipherDomain, file, userId);
} }
protected deleteCipherAttachment(attachmentId: string) { protected deleteCipherAttachment(attachmentId: string, userId: UserId) {
return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId); return this.cipherService.deleteAttachmentWithServer(this.cipher.id, attachmentId, userId);
} }
protected async reupload(attachment: AttachmentView) { protected async reupload(attachment: AttachmentView) {

View File

@@ -1,9 +1,10 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { Directive, OnInit } from "@angular/core"; 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 { 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -39,10 +40,8 @@ export class PasswordHistoryComponent implements OnInit {
} }
protected async init() { protected async init() {
const cipher = await this.cipherService.get(this.cipherId); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const activeUserId = await firstValueFrom( const cipher = await this.cipherService.get(this.cipherId, activeUserId);
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const decCipher = await cipher.decrypt( const decCipher = await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );

View File

@@ -2,10 +2,13 @@
// @ts-strict-ignore // @ts-strict-ignore
import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core"; import { Directive, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; 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 { SearchService } from "@bitwarden/common/abstractions/search.service";
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -41,9 +44,18 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
constructor( constructor(
protected searchService: SearchService, protected searchService: SearchService,
protected cipherService: CipherService, protected cipherService: CipherService,
protected accountService: AccountService,
) { ) {
this.cipherService.cipherViews$.pipe(takeUntilDestroyed()).subscribe((ciphers) => { this.accountService.activeAccount$
void this.doSearch(ciphers); .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; this.loaded = true;
}); });
} }
@@ -122,10 +134,16 @@ export class VaultItemsComponent implements OnInit, OnDestroy {
protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted; protected deletedFilter: (cipher: CipherView) => boolean = (c) => c.isDeleted === this.deleted;
protected async doSearch(indexedCiphers?: CipherView[]) { protected async doSearch(indexedCiphers?: CipherView[], userId?: UserId) {
indexedCiphers = indexedCiphers ?? (await firstValueFrom(this.cipherService.cipherViews$)); // 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) { if (failedCiphers != null && failedCiphers.length > 0) {
indexedCiphers = [...failedCiphers, ...indexedCiphers]; indexedCiphers = [...failedCiphers, ...indexedCiphers];
} }

View File

@@ -18,6 +18,7 @@ import { AuditService } from "@bitwarden/common/abstractions/audit.service";
import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service"; import { EventCollectionService } from "@bitwarden/common/abstractions/event/event-collection.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { TokenService } from "@bitwarden/common/auth/abstractions/token.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 { BillingAccountProfileStateService } from "@bitwarden/common/billing/abstractions/account/billing-account-profile-state.service";
import { EventType } from "@bitwarden/common/enums"; import { EventType } from "@bitwarden/common/enums";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { StateService } from "@bitwarden/common/platform/abstractions/state.service"; import { StateService } from "@bitwarden/common/platform/abstractions/state.service";
import { EncArrayBuffer } from "@bitwarden/common/platform/models/domain/enc-array-buffer"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service"; import { TotpService } from "@bitwarden/common/vault/abstractions/totp.service";
@@ -79,7 +80,6 @@ export class ViewComponent implements OnDestroy, OnInit {
private previousCipherId: string; private previousCipherId: string;
private passwordReprompted = false; private passwordReprompted = false;
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
private destroyed$ = new Subject<void>(); private destroyed$ = new Subject<void>();
get fido2CredentialCreationDateValue(): string { get fido2CredentialCreationDateValue(): string {
@@ -144,9 +144,10 @@ export class ViewComponent implements OnDestroy, OnInit {
async load() { async load() {
this.cleanUp(); 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 // Grab individual cipher from `cipherViews$` for the most up-to-date information
this.cipherService.cipherViews$ this.cipherService
.cipherViews$(activeUserId)
.pipe( .pipe(
map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)), map((ciphers) => ciphers?.find((c) => c.id === this.cipherId)),
filter((cipher) => !!cipher), filter((cipher) => !!cipher),
@@ -250,7 +251,8 @@ export class ViewComponent implements OnDestroy, OnInit {
} }
try { try {
await this.deleteCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.deleteCipher(activeUserId);
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
@@ -272,7 +274,8 @@ export class ViewComponent implements OnDestroy, OnInit {
} }
try { try {
await this.restoreCipher(); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
await this.restoreCipher(activeUserId);
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
title: null, title: null,
@@ -380,7 +383,8 @@ export class ViewComponent implements OnDestroy, OnInit {
} }
if (cipherId) { 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); this.platformUtilsService.launchUri(uri.launchUri);
@@ -498,14 +502,14 @@ export class ViewComponent implements OnDestroy, OnInit {
a.downloading = false; a.downloading = false;
} }
protected deleteCipher() { protected deleteCipher(userId: UserId) {
return this.cipher.isDeleted return this.cipher.isDeleted
? this.cipherService.deleteWithServer(this.cipher.id) ? this.cipherService.deleteWithServer(this.cipher.id, userId)
: this.cipherService.softDeleteWithServer(this.cipher.id); : this.cipherService.softDeleteWithServer(this.cipher.id, userId);
} }
protected restoreCipher() { protected restoreCipher(userId: UserId) {
return this.cipherService.restoreWithServer(this.cipher.id); return this.cipherService.restoreWithServer(this.cipher.id, userId);
} }
protected async promptPassword() { protected async promptPassword() {

View File

@@ -11,6 +11,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service";
import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; 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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node";
@@ -30,8 +31,6 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
private readonly collapsedGroupings$: Observable<Set<string>> = private readonly collapsedGroupings$: Observable<Set<string>> =
this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c)));
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
protected organizationService: OrganizationService, protected organizationService: OrganizationService,
protected folderService: FolderService, protected folderService: FolderService,
@@ -63,7 +62,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
} }
buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> { buildNestedFolders(organizationId?: string): Observable<DynamicTreeNode<FolderView>> {
const transformation = async (storedFolders: FolderView[]) => { const transformation = async (storedFolders: FolderView[], userId: UserId) => {
let folders: FolderView[]; let folders: FolderView[];
// If no org or "My Vault" is selected, show all folders // If no org or "My Vault" is selected, show all folders
@@ -71,7 +70,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
folders = storedFolders; folders = storedFolders;
} else { } else {
// Otherwise, show only folders that have ciphers from the selected org and the "no folder" folder // 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); const orgCiphers = ciphers.filter((c) => c.organizationId == organizationId);
folders = storedFolders.filter( folders = storedFolders.filter(
(f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null, (f) => orgCiphers.some((oc) => oc.folderId == f.id) || f.id == null,
@@ -85,9 +84,13 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti
}); });
}; };
return this.activeUserId$.pipe( return this.accountService.activeAccount$.pipe(
switchMap((userId) => this.folderService.folderViews$(userId)), getUserId,
mergeMap((folders) => from(transformation(folders))), 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<TreeNode<FolderView>> { async getFolderNested(id: string): Promise<TreeNode<FolderView>> {
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const folders = await this.getAllFoldersNested( const folders = await this.getAllFoldersNested(
await firstValueFrom(this.folderService.folderViews$(activeUserId)), await firstValueFrom(this.folderService.folderViews$(activeUserId)),
); );

View File

@@ -151,11 +151,15 @@ export class DefaultNotificationsService implements NotificationsServiceAbstract
await this.syncService.syncUpsertCipher( await this.syncService.syncUpsertCipher(
notification.payload as SyncCipherNotification, notification.payload as SyncCipherNotification,
notification.type === NotificationType.SyncCipherUpdate, notification.type === NotificationType.SyncCipherUpdate,
payloadUserId,
); );
break; break;
case NotificationType.SyncCipherDelete: case NotificationType.SyncCipherDelete:
case NotificationType.SyncLoginDelete: case NotificationType.SyncLoginDelete:
await this.syncService.syncDeleteCipher(notification.payload as SyncCipherNotification); await this.syncService.syncDeleteCipher(
notification.payload as SyncCipherNotification,
payloadUserId,
);
break; break;
case NotificationType.SyncFolderCreate: case NotificationType.SyncFolderCreate:
case NotificationType.SyncFolderUpdate: case NotificationType.SyncFolderUpdate:

View File

@@ -3,7 +3,8 @@ import { TextEncoder } from "util";
import { mock, MockProxy } from "jest-mock-extended"; import { mock, MockProxy } from "jest-mock-extended";
import { BehaviorSubject, of } from "rxjs"; 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 { UserId } from "../../../types/guid";
import { CipherService } from "../../../vault/abstractions/cipher.service"; import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
@@ -46,7 +47,6 @@ describe("FidoAuthenticatorService", () => {
let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>; let userInterface!: MockProxy<Fido2UserInterfaceService<ParentWindowReference>>;
let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>; let userInterfaceSession!: MockProxy<Fido2UserInterfaceSession>;
let syncService!: MockProxy<SyncService>; let syncService!: MockProxy<SyncService>;
let accountService!: MockProxy<AccountService>;
let authenticator!: Fido2AuthenticatorService<ParentWindowReference>; let authenticator!: Fido2AuthenticatorService<ParentWindowReference>;
let windowReference!: ParentWindowReference; let windowReference!: ParentWindowReference;
@@ -58,7 +58,7 @@ describe("FidoAuthenticatorService", () => {
syncService = mock<SyncService>({ syncService = mock<SyncService>({
activeUserLastSync$: () => of(new Date()), activeUserLastSync$: () => of(new Date()),
}); });
accountService = mock<AccountService>(); const accountService = mockAccountServiceWith("testId" as UserId);
authenticator = new Fido2AuthenticatorService( authenticator = new Fido2AuthenticatorService(
cipherService, cipherService,
userInterface, userInterface,

View File

@@ -1,8 +1,9 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { AccountService } from "../../../auth/abstractions/account.service"; import { AccountService } from "../../../auth/abstractions/account.service";
import { getUserId } from "../../../auth/services/account.service";
import { CipherService } from "../../../vault/abstractions/cipher.service"; import { CipherService } from "../../../vault/abstractions/cipher.service";
import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction"; import { SyncService } from "../../../vault/abstractions/sync/sync.service.abstraction";
import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type"; import { CipherRepromptType } from "../../../vault/enums/cipher-reprompt-type";
@@ -145,10 +146,10 @@ export class Fido2AuthenticatorService<ParentWindowReference>
try { try {
keyPair = await createKeyPair(); keyPair = await createKeyPair();
pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey); pubKeyDer = await crypto.subtle.exportKey("spki", keyPair.publicKey);
const encrypted = await this.cipherService.get(cipherId);
const activeUserId = await firstValueFrom( 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( cipher = await encrypted.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(encrypted, activeUserId),
@@ -309,7 +310,7 @@ export class Fido2AuthenticatorService<ParentWindowReference>
if (selectedFido2Credential.counter > 0) { if (selectedFido2Credential.counter > 0) {
const activeUserId = await firstValueFrom( 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); const encrypted = await this.cipherService.encrypt(selectedCipher, activeUserId);
await this.cipherService.updateWithServer(encrypted); await this.cipherService.updateWithServer(encrypted);
@@ -400,7 +401,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
return []; 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 return ciphers
.filter( .filter(
(cipher) => (cipher) =>
@@ -421,7 +423,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
return []; 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( return ciphers.filter(
(cipher) => (cipher) =>
!cipher.isDeleted && !cipher.isDeleted &&
@@ -438,7 +441,8 @@ export class Fido2AuthenticatorService<ParentWindowReference>
} }
private async findCredentialsByRp(rpId: string): Promise<CipherView[]> { private async findCredentialsByRp(rpId: string): Promise<CipherView[]> {
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( return ciphers.filter(
(cipher) => (cipher) =>
!cipher.isDeleted && !cipher.isDeleted &&

View File

@@ -129,12 +129,18 @@ export abstract class CoreSyncService implements SyncService {
return this.syncCompleted(false); return this.syncCompleted(false);
} }
async syncUpsertCipher(notification: SyncCipherNotification, isEdit: boolean): Promise<boolean> { async syncUpsertCipher(
notification: SyncCipherNotification,
isEdit: boolean,
userId: UserId,
): Promise<boolean> {
this.syncStarted(); this.syncStarted();
if (await this.stateService.getIsAuthenticated()) {
const authStatus = await firstValueFrom(this.authService.authStatusFor$(userId));
if (authStatus >= AuthenticationStatus.Locked) {
try { try {
let shouldUpdate = true; 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) { if (localCipher != null && localCipher.revisionDate >= notification.revisionDate) {
shouldUpdate = false; shouldUpdate = false;
} }
@@ -182,7 +188,7 @@ export abstract class CoreSyncService implements SyncService {
} }
} catch (e) { } catch (e) {
if (e != null && e.statusCode === 404 && isEdit) { 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 }); this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true); return this.syncCompleted(true);
} }
@@ -191,10 +197,12 @@ export abstract class CoreSyncService implements SyncService {
return this.syncCompleted(false); return this.syncCompleted(false);
} }
async syncDeleteCipher(notification: SyncCipherNotification): Promise<boolean> { async syncDeleteCipher(notification: SyncCipherNotification, userId: UserId): Promise<boolean> {
this.syncStarted(); 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 }); this.messageSender.send("syncedDeletedCipher", { cipherId: notification.id });
return this.syncCompleted(true); return this.syncCompleted(true);
} }

View File

@@ -62,8 +62,9 @@ export abstract class SyncService {
abstract syncUpsertCipher( abstract syncUpsertCipher(
notification: SyncCipherNotification, notification: SyncCipherNotification,
isEdit: boolean, isEdit: boolean,
userId: UserId,
): Promise<boolean>; ): Promise<boolean>;
abstract syncDeleteCipher(notification: SyncFolderNotification): Promise<boolean>; abstract syncDeleteCipher(notification: SyncFolderNotification, userId: UserId): Promise<boolean>;
abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean>; abstract syncUpsertSend(notification: SyncSendNotification, isEdit: boolean): Promise<boolean>;
abstract syncDeleteSend(notification: SyncSendNotification): Promise<boolean>; abstract syncDeleteSend(notification: SyncSendNotification): Promise<boolean>;
} }

View File

@@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction
import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/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 { EventCollectionService as EventCollectionServiceAbstraction } from "../../abstractions/event/event-collection.service";
import { EventUploadService } from "../../abstractions/event/event-upload.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 userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION);
if (!(await this.shouldUpdate(null, eventType, ciphers))) { if (!(await this.shouldUpdate(userId, null, eventType, ciphers))) {
return; return;
} }
@@ -91,7 +92,7 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$)); const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const eventStore = this.stateProvider.getUser(userId, EVENT_COLLECTION); 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; return;
} }
@@ -113,18 +114,18 @@ export class EventCollectionService implements EventCollectionServiceAbstraction
} }
/** Verifies if the event collection should be updated for the provided information /** 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 cipherId the cipher for the event
* @param organizationId the organization for the event * @param organizationId the organization for the event
*/ */
private async shouldUpdate( private async shouldUpdate(
userId: UserId,
organizationId: string = null, organizationId: string = null,
eventType: EventType = null, eventType: EventType = null,
ciphers: CipherView[] = [], ciphers: CipherView[] = [],
cipherId?: string, cipherId?: string,
): Promise<boolean> { ): Promise<boolean> {
const cipher$ = from(this.cipherService.get(cipherId)); const cipher$ = from(this.cipherService.get(cipherId, userId));
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
const orgIds$ = this.organizationService const orgIds$ = this.organizationService
.organizations$(userId) .organizations$(userId)

View File

@@ -19,57 +19,70 @@ import { FieldView } from "../models/view/field.view";
import { AddEditCipherInfo } from "../types/add-edit-cipher-info"; import { AddEditCipherInfo } from "../types/add-edit-cipher-info";
export abstract class CipherService implements UserKeyRotationDataProvider<CipherWithIdRequest> { export abstract class CipherService implements UserKeyRotationDataProvider<CipherWithIdRequest> {
cipherViews$: Observable<CipherView[]>; abstract cipherViews$(userId: UserId): Observable<CipherView[]>;
ciphers$: Observable<Record<CipherId, CipherData>>; abstract ciphers$(userId: UserId): Observable<Record<CipherId, CipherData>>;
localData$: Observable<Record<CipherId, LocalData>>; abstract localData$(userId: UserId): Observable<Record<CipherId, LocalData>>;
/** /**
* An observable monitoring the add/edit cipher info saved to memory. * An observable monitoring the add/edit cipher info saved to memory.
*/ */
addEditCipherInfo$: Observable<AddEditCipherInfo>; abstract addEditCipherInfo$(userId: UserId): Observable<AddEditCipherInfo>;
/** /**
* Observable that emits an array of cipherViews that failed to decrypt. Does not emit until decryption has completed. * 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. * An empty array indicates that all ciphers were successfully decrypted.
*/ */
failedToDecryptCiphers$: Observable<CipherView[]>; abstract failedToDecryptCiphers$(userId: UserId): Observable<CipherView[]>;
clearCache: (userId?: string) => Promise<void>; abstract clearCache(userId: UserId): Promise<void>;
encrypt: ( abstract encrypt(
model: CipherView, model: CipherView,
userId: UserId, userId: UserId,
keyForEncryption?: SymmetricCryptoKey, keyForEncryption?: SymmetricCryptoKey,
keyForCipherKeyDecryption?: SymmetricCryptoKey, keyForCipherKeyDecryption?: SymmetricCryptoKey,
originalCipher?: Cipher, originalCipher?: Cipher,
) => Promise<Cipher>; ): Promise<Cipher>;
encryptFields: (fieldsModel: FieldView[], key: SymmetricCryptoKey) => Promise<Field[]>; abstract encryptFields(fieldsModel: FieldView[], key: SymmetricCryptoKey): Promise<Field[]>;
encryptField: (fieldModel: FieldView, key: SymmetricCryptoKey) => Promise<Field>; abstract encryptField(fieldModel: FieldView, key: SymmetricCryptoKey): Promise<Field>;
get: (id: string) => Promise<Cipher>; abstract get(id: string, userId: UserId): Promise<Cipher>;
getAll: () => Promise<Cipher[]>; abstract getAll(userId: UserId): Promise<Cipher[]>;
getAllDecrypted: () => Promise<CipherView[]>; abstract getAllDecrypted(userId: UserId): Promise<CipherView[]>;
getAllDecryptedForGrouping: (groupingId: string, folder?: boolean) => Promise<CipherView[]>; abstract getAllDecryptedForGrouping(
getAllDecryptedForUrl: ( groupingId: string,
userId: UserId,
folder?: boolean,
): Promise<CipherView[]>;
abstract getAllDecryptedForUrl(
url: string, url: string,
userId: UserId,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchStrategySetting, defaultMatch?: UriMatchStrategySetting,
) => Promise<CipherView[]>; ): Promise<CipherView[]>;
filterCiphersForUrl: ( abstract filterCiphersForUrl(
ciphers: CipherView[], ciphers: CipherView[],
url: string, url: string,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch?: UriMatchStrategySetting, defaultMatch?: UriMatchStrategySetting,
) => Promise<CipherView[]>; ): Promise<CipherView[]>;
getAllFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; abstract getAllFromApiForOrganization(organizationId: string): Promise<CipherView[]>;
/** /**
* Gets ciphers belonging to the specified organization that the user has explicit collection level access to. * 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. * Ciphers that are not assigned to any collections are only included for users with admin access.
*/ */
getManyFromApiForOrganization: (organizationId: string) => Promise<CipherView[]>; abstract getManyFromApiForOrganization(organizationId: string): Promise<CipherView[]>;
getLastUsedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>; abstract getLastUsedForUrl(
getLastLaunchedForUrl: (url: string, autofillOnPageLoad: boolean) => Promise<CipherView>; url: string,
getNextCipherForUrl: (url: string) => Promise<CipherView>; userId: UserId,
updateLastUsedIndexForUrl: (url: string) => void; autofillOnPageLoad: boolean,
updateLastUsedDate: (id: string) => Promise<void>; ): Promise<CipherView>;
updateLastLaunchedDate: (id: string) => Promise<void>; abstract getLastLaunchedForUrl(
saveNeverDomain: (domain: string) => Promise<void>; url: string,
userId: UserId,
autofillOnPageLoad: boolean,
): Promise<CipherView>;
abstract getNextCipherForUrl(url: string, userId: UserId): Promise<CipherView>;
abstract updateLastUsedIndexForUrl(url: string): void;
abstract updateLastUsedDate(id: string, userId: UserId): Promise<void>;
abstract updateLastLaunchedDate(id: string, userId: UserId): Promise<void>;
abstract saveNeverDomain(domain: string): Promise<void>;
/** /**
* Create a cipher with the server * Create a cipher with the server
* *
@@ -78,7 +91,7 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* *
* @returns A promise that resolves to the created cipher * @returns A promise that resolves to the created cipher
*/ */
createWithServer: (cipher: Cipher, orgAdmin?: boolean) => Promise<Cipher>; abstract createWithServer(cipher: Cipher, orgAdmin?: boolean): Promise<Cipher>;
/** /**
* Update a cipher with the server * Update a cipher with the server
* @param cipher The cipher to update * @param cipher The cipher to update
@@ -87,88 +100,105 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* *
* @returns A promise that resolves to the updated cipher * @returns A promise that resolves to the updated cipher
*/ */
updateWithServer: (cipher: Cipher, orgAdmin?: boolean, isNotClone?: boolean) => Promise<Cipher>; abstract updateWithServer(
shareWithServer: ( cipher: Cipher,
orgAdmin?: boolean,
isNotClone?: boolean,
): Promise<Cipher>;
abstract shareWithServer(
cipher: CipherView, cipher: CipherView,
organizationId: string, organizationId: string,
collectionIds: string[], collectionIds: string[],
userId: UserId, userId: UserId,
) => Promise<Cipher>; ): Promise<Cipher>;
shareManyWithServer: ( abstract shareManyWithServer(
ciphers: CipherView[], ciphers: CipherView[],
organizationId: string, organizationId: string,
collectionIds: string[], collectionIds: string[],
userId: UserId, userId: UserId,
) => Promise<any>; ): Promise<any>;
saveAttachmentWithServer: ( abstract saveAttachmentWithServer(
cipher: Cipher, cipher: Cipher,
unencryptedFile: any, unencryptedFile: any,
userId: UserId, userId: UserId,
admin?: boolean, admin?: boolean,
) => Promise<Cipher>; ): Promise<Cipher>;
saveAttachmentRawWithServer: ( abstract saveAttachmentRawWithServer(
cipher: Cipher, cipher: Cipher,
filename: string, filename: string,
data: ArrayBuffer, data: ArrayBuffer,
userId: UserId, userId: UserId,
admin?: boolean, admin?: boolean,
) => Promise<Cipher>; ): Promise<Cipher>;
/** /**
* Save the collections for a cipher with the server * Save the collections for a cipher with the server
* *
* @param cipher The cipher to save collections for * @param cipher The cipher to save collections for
* @param userId The user ID
* *
* @returns A promise that resolves when the collections have been saved * @returns A promise that resolves when the collections have been saved
*/ */
saveCollectionsWithServer: (cipher: Cipher) => Promise<Cipher>; abstract saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise<Cipher>;
/** /**
* Save the collections for a cipher with the server as an admin. * 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). * Used for Unassigned ciphers or when the user only has admin access to the cipher (not assigned normally).
* @param cipher * @param cipher
*/ */
saveCollectionsWithServerAdmin: (cipher: Cipher) => Promise<Cipher>; abstract saveCollectionsWithServerAdmin(cipher: Cipher): Promise<Cipher>;
/** /**
* Bulk update collections for many ciphers with the server * Bulk update collections for many ciphers with the server
* @param orgId * @param orgId
* @param userId
* @param cipherIds * @param cipherIds
* @param collectionIds * @param collectionIds
* @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added * @param removeCollections - If true, the collections will be removed from the ciphers, otherwise they will be added
*/ */
bulkUpdateCollectionsWithServer: ( abstract bulkUpdateCollectionsWithServer(
orgId: OrganizationId, orgId: OrganizationId,
userId: UserId,
cipherIds: CipherId[], cipherIds: CipherId[],
collectionIds: CollectionId[], collectionIds: CollectionId[],
removeCollections: boolean, removeCollections: boolean,
) => Promise<void>; ): Promise<void>;
/** /**
* Update the local store of CipherData with the provided data. Values are upserted into the existing store. * 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. * @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 * @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<Record<CipherId, CipherData>>; abstract upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>>;
replace: (ciphers: { [id: string]: CipherData }, userId: UserId) => Promise<any>; abstract replace(ciphers: { [id: string]: CipherData }, userId: UserId): Promise<any>;
clear: (userId?: string) => Promise<void>; abstract clear(userId?: string): Promise<void>;
moveManyWithServer: (ids: string[], folderId: string) => Promise<any>; abstract moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise<any>;
delete: (id: string | string[]) => Promise<any>; abstract delete(id: string | string[], userId: UserId): Promise<any>;
deleteWithServer: (id: string, asAdmin?: boolean) => Promise<any>; abstract deleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
deleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise<any>; abstract deleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise<any>;
deleteAttachment: (id: string, revisionDate: string, attachmentId: string) => Promise<CipherData>; abstract deleteAttachment(
deleteAttachmentWithServer: (id: string, attachmentId: string) => Promise<CipherData>; id: string,
sortCiphersByLastUsed: (a: CipherView, b: CipherView) => number; revisionDate: string,
sortCiphersByLastUsedThenName: (a: CipherView, b: CipherView) => number; attachmentId: string,
getLocaleSortingFunction: () => (a: CipherView, b: CipherView) => number; userId: UserId,
softDelete: (id: string | string[]) => Promise<any>; ): Promise<CipherData>;
softDeleteWithServer: (id: string, asAdmin?: boolean) => Promise<any>; abstract deleteAttachmentWithServer(
softDeleteManyWithServer: (ids: string[], asAdmin?: boolean) => Promise<any>; id: string,
restore: ( attachmentId: string,
userId: UserId,
): Promise<CipherData>;
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<any>;
abstract softDeleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
abstract softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin?: boolean): Promise<any>;
abstract restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[],
) => Promise<any>; userId: UserId,
restoreWithServer: (id: string, asAdmin?: boolean) => Promise<any>; ): Promise<any>;
restoreManyWithServer: (ids: string[], orgId?: string) => Promise<void>; abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<any>;
getKeyForCipherKeyDecryption: (cipher: Cipher, userId: UserId) => Promise<any>; abstract restoreManyWithServer(ids: string[], orgId?: string): Promise<void>;
setAddEditCipherInfo: (value: AddEditCipherInfo) => Promise<void>; abstract getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<any>;
abstract setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId): Promise<void>;
/** /**
* Returns user ciphers re-encrypted with the new user key. * Returns user ciphers re-encrypted with the new user key.
* @param originalUserKey the original user key * @param originalUserKey the original user key
@@ -177,11 +207,11 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
* @throws Error if new user key is null * @throws Error if new user key is null
* @returns a list of user ciphers that have been re-encrypted with the new user key * @returns a list of user ciphers that have been re-encrypted with the new user key
*/ */
getRotatedData: ( abstract getRotatedData(
originalUserKey: UserKey, originalUserKey: UserKey,
newUserKey: UserKey, newUserKey: UserKey,
userId: UserId, userId: UserId,
) => Promise<CipherWithIdRequest[]>; ): Promise<CipherWithIdRequest[]>;
getNextCardCipher: () => Promise<CipherView>; abstract getNextCardCipher(userId: UserId): Promise<CipherView>;
getNextIdentityCipher: () => Promise<CipherView>; abstract getNextIdentityCipher(userId: UserId): Promise<CipherView>;
} }

View File

@@ -382,8 +382,16 @@ describe("Cipher Service", () => {
Cipher1: cipher1, Cipher1: cipher1,
Cipher2: cipher2, Cipher2: cipher2,
}); });
cipherService.cipherViews$ = decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))); jest
cipherService.failedToDecryptCiphers$ = failedCiphers = new BehaviorSubject<CipherView[]>([]); .spyOn(cipherService, "cipherViews$")
.mockImplementation((userId: UserId) =>
decryptedCiphers.pipe(map((ciphers) => Object.values(ciphers))),
);
failedCiphers = new BehaviorSubject<CipherView[]>([]);
jest
.spyOn(cipherService, "failedToDecryptCiphers$")
.mockImplementation((userId: UserId) => failedCiphers);
encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32)); encryptService.decryptToBytes.mockResolvedValue(new Uint8Array(32));
encryptedKey = new EncString("Re-encrypted Cipher Key"); encryptedKey = new EncString("Re-encrypted Cipher Key");

View File

@@ -37,7 +37,7 @@ import Domain from "../../platform/models/domain/domain-base";
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer"; import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
import { EncString } from "../../platform/models/domain/enc-string"; import { EncString } from "../../platform/models/domain/enc-string";
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key"; 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 { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid";
import { OrgKey, UserKey } from "../../types/key"; import { OrgKey, UserKey } from "../../types/key";
import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service"; import { CipherService as CipherServiceAbstraction } from "../abstractions/cipher.service";
@@ -97,33 +97,6 @@ export class CipherService implements CipherServiceAbstraction {
*/ */
private forceCipherViews$: Subject<CipherView[]> = new Subject<CipherView[]>(); private forceCipherViews$: Subject<CipherView[]> = new Subject<CipherView[]>();
localData$: Observable<Record<CipherId, LocalData>>;
ciphers$: Observable<Record<CipherId, CipherData>>;
/**
* 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<CipherView[] | null>;
addEditCipherInfo$: Observable<AddEditCipherInfo>;
/**
* 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<CipherView[]>;
private localDataState: ActiveUserState<Record<CipherId, LocalData>>;
private encryptedCiphersState: ActiveUserState<Record<CipherId, CipherData>>;
private decryptedCiphersState: ActiveUserState<Record<CipherId, CipherView>>;
private failedToDecryptCiphersState: ActiveUserState<CipherView[]>;
private addEditCipherInfoState: ActiveUserState<AddEditCipherInfo>;
constructor( constructor(
private keyService: KeyService, private keyService: KeyService,
private domainSettingsService: DomainSettingsService, private domainSettingsService: DomainSettingsService,
@@ -138,30 +111,49 @@ export class CipherService implements CipherServiceAbstraction {
private configService: ConfigService, private configService: ConfigService,
private stateProvider: StateProvider, private stateProvider: StateProvider,
private accountService: AccountService, 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 ?? {})); localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
this.ciphers$ = this.encryptedCiphersState.state$.pipe(map((ciphers) => ciphers ?? {})); 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<Record<CipherId, CipherData>> {
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<CipherView[] | null> {
return combineLatest([this.encryptedCiphersState(userId).state$, this.localData$(userId)]).pipe(
filter(([ciphers]) => ciphers != null), // Skip if ciphers haven't been loaded yor synced yet 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 }), shareReplay({ bufferSize: 1, refCount: true }),
); );
}
this.failedToDecryptCiphers$ = this.failedToDecryptCiphersState.state$.pipe( addEditCipherInfo$(userId: UserId): Observable<AddEditCipherInfo> {
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<CipherView[]> {
return this.failedToDecryptCiphersState(userId).state$.pipe(
filter((ciphers) => ciphers != null), filter((ciphers) => ciphers != null),
switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))), switchMap((ciphers) => merge(this.forceCipherViews$, of(ciphers))),
shareReplay({ bufferSize: 1, refCount: true }), shareReplay({ bufferSize: 1, refCount: true }),
); );
this.addEditCipherInfo$ = this.addEditCipherInfoState.state$;
} }
async setDecryptedCipherCache(value: CipherView[], userId: UserId) { async setDecryptedCipherCache(value: CipherView[], userId: UserId) {
@@ -212,7 +204,7 @@ export class CipherService implements CipherServiceAbstraction {
): Promise<Cipher> { ): Promise<Cipher> {
if (model.id != null) { if (model.id != null) {
if (originalCipher == null) { if (originalCipher == null) {
originalCipher = await this.get(model.id); originalCipher = await this.get(model.id, userId);
} }
if (originalCipher != null) { if (originalCipher != null) {
await this.updateModelfromExistingCipher(model, originalCipher, userId); await this.updateModelfromExistingCipher(model, originalCipher, userId);
@@ -366,22 +358,22 @@ export class CipherService implements CipherServiceAbstraction {
return ph; return ph;
} }
async get(id: string): Promise<Cipher> { async get(id: string, userId: UserId): Promise<Cipher> {
const ciphers = await firstValueFrom(this.ciphers$); const ciphers = await firstValueFrom(this.ciphers$(userId));
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id)) { if (ciphers == null || !ciphers.hasOwnProperty(id)) {
return null; return null;
} }
const localData = await firstValueFrom(this.localData$); const localData = await firstValueFrom(this.localData$(userId));
const cipherId = id as CipherId; const cipherId = id as CipherId;
return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null); return new Cipher(ciphers[cipherId], localData ? localData[cipherId] : null);
} }
async getAll(): Promise<Cipher[]> { async getAll(userId: UserId): Promise<Cipher[]> {
const localData = await firstValueFrom(this.localData$); const localData = await firstValueFrom(this.localData$(userId));
const ciphers = await firstValueFrom(this.ciphers$); const ciphers = await firstValueFrom(this.ciphers$(userId));
const response: Cipher[] = []; const response: Cipher[] = [];
for (const id in ciphers) { for (const id in ciphers) {
// eslint-disable-next-line // eslint-disable-next-line
@@ -399,33 +391,27 @@ export class CipherService implements CipherServiceAbstraction {
* @deprecated Use `cipherViews$` observable instead * @deprecated Use `cipherViews$` observable instead
*/ */
@sequentialize(() => "getAllDecrypted") @sequentialize(() => "getAllDecrypted")
async getAllDecrypted(): Promise<CipherView[]> { async getAllDecrypted(userId: UserId): Promise<CipherView[]> {
const decCiphers = await this.getDecryptedCiphers(); const decCiphers = await this.getDecryptedCiphers(userId);
if (decCiphers != null && decCiphers.length !== 0) { if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers(); await this.reindexCiphers(userId);
return await this.getDecryptedCiphers(); return await this.getDecryptedCiphers(userId);
}
const activeUserId = await firstValueFrom(this.stateProvider.activeUserId$);
if (activeUserId == null) {
return [];
} }
const [newDecCiphers, failedCiphers] = await this.decryptCiphers( const [newDecCiphers, failedCiphers] = await this.decryptCiphers(
await this.getAll(), await this.getAll(userId),
activeUserId, userId,
); );
await this.setDecryptedCipherCache(newDecCiphers, activeUserId); await this.setDecryptedCipherCache(newDecCiphers, userId);
await this.setFailedDecryptedCiphers(failedCiphers, activeUserId); await this.setFailedDecryptedCiphers(failedCiphers, userId);
return newDecCiphers; return newDecCiphers;
} }
private async getDecryptedCiphers() { private async getDecryptedCiphers(userId: UserId) {
return Object.values( 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() { private async reindexCiphers(userId: UserId) {
const userId = await this.stateService.getUserId();
const reindexRequired = const reindexRequired =
this.searchService != null && this.searchService != null &&
((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId; ((await firstValueFrom(this.searchService.indexedEntityId$)) ?? userId) !== userId;
if (reindexRequired) { 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<CipherView[]> { async getAllDecryptedForGrouping(
const ciphers = await this.getAllDecrypted(); groupingId: string,
userId: UserId,
folder = true,
): Promise<CipherView[]> {
const ciphers = await this.getAllDecrypted(userId);
return ciphers.filter((cipher) => { return ciphers.filter((cipher) => {
if (cipher.isDeleted) { if (cipher.isDeleted) {
@@ -524,10 +513,11 @@ export class CipherService implements CipherServiceAbstraction {
async getAllDecryptedForUrl( async getAllDecryptedForUrl(
url: string, url: string,
userId: UserId,
includeOtherTypes?: CipherType[], includeOtherTypes?: CipherType[],
defaultMatch: UriMatchStrategySetting = null, defaultMatch: UriMatchStrategySetting = null,
): Promise<CipherView[]> { ): Promise<CipherView[]> {
const ciphers = await this.getAllDecrypted(); const ciphers = await this.getAllDecrypted(userId);
return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch); return await this.filterCiphersForUrl(ciphers, url, includeOtherTypes, defaultMatch);
} }
@@ -569,8 +559,11 @@ export class CipherService implements CipherServiceAbstraction {
}); });
} }
private async getAllDecryptedCiphersOfType(type: CipherType[]): Promise<CipherView[]> { private async getAllDecryptedCiphersOfType(
const ciphers = await this.getAllDecrypted(); type: CipherType[],
userId: UserId,
): Promise<CipherView[]> {
const ciphers = await this.getAllDecrypted(userId);
return ciphers return ciphers
.filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type)) .filter((cipher) => cipher.deletedDate == null && type.includes(cipher.type))
.sort((a, b) => this.sortCiphersByLastUsedThenName(a, b)); .sort((a, b) => this.sortCiphersByLastUsedThenName(a, b));
@@ -613,23 +606,31 @@ export class CipherService implements CipherServiceAbstraction {
return decCiphers; return decCiphers;
} }
async getLastUsedForUrl(url: string, autofillOnPageLoad = false): Promise<CipherView> { async getLastUsedForUrl(
return this.getCipherForUrl(url, true, false, autofillOnPageLoad); url: string,
userId: UserId,
autofillOnPageLoad = false,
): Promise<CipherView> {
return this.getCipherForUrl(url, userId, true, false, autofillOnPageLoad);
} }
async getLastLaunchedForUrl(url: string, autofillOnPageLoad = false): Promise<CipherView> { async getLastLaunchedForUrl(
return this.getCipherForUrl(url, false, true, autofillOnPageLoad); url: string,
userId: UserId,
autofillOnPageLoad = false,
): Promise<CipherView> {
return this.getCipherForUrl(url, userId, false, true, autofillOnPageLoad);
} }
async getNextCipherForUrl(url: string): Promise<CipherView> { async getNextCipherForUrl(url: string, userId: UserId): Promise<CipherView> {
return this.getCipherForUrl(url, false, false, false); return this.getCipherForUrl(url, userId, false, false, false);
} }
async getNextCardCipher(): Promise<CipherView> { async getNextCardCipher(userId: UserId): Promise<CipherView> {
const cacheKey = "cardCiphers"; const cacheKey = "cardCiphers";
if (!this.sortedCiphersCache.isCached(cacheKey)) { if (!this.sortedCiphersCache.isCached(cacheKey)) {
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card]); const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Card], userId);
if (!ciphers?.length) { if (!ciphers?.length) {
return null; return null;
} }
@@ -640,11 +641,11 @@ export class CipherService implements CipherServiceAbstraction {
return this.sortedCiphersCache.getNext(cacheKey); return this.sortedCiphersCache.getNext(cacheKey);
} }
async getNextIdentityCipher(): Promise<CipherView> { async getNextIdentityCipher(userId: UserId): Promise<CipherView> {
const cacheKey = "identityCiphers"; const cacheKey = "identityCiphers";
if (!this.sortedCiphersCache.isCached(cacheKey)) { if (!this.sortedCiphersCache.isCached(cacheKey)) {
const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity]); const ciphers = await this.getAllDecryptedCiphersOfType([CipherType.Identity], userId);
if (!ciphers?.length) { if (!ciphers?.length) {
return null; return null;
} }
@@ -659,9 +660,8 @@ export class CipherService implements CipherServiceAbstraction {
this.sortedCiphersCache.updateLastUsedIndex(url); this.sortedCiphersCache.updateLastUsedIndex(url);
} }
async updateLastUsedDate(id: string): Promise<void> { async updateLastUsedDate(id: string, userId: UserId): Promise<void> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$); let ciphersLocalData = await firstValueFrom(this.localData$(userId));
let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) { if (!ciphersLocalData) {
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) { if (!decryptedCipherCache) {
return; return;
} }
@@ -693,9 +693,8 @@ export class CipherService implements CipherServiceAbstraction {
await this.setDecryptedCiphers(decryptedCipherCache, userId); await this.setDecryptedCiphers(decryptedCipherCache, userId);
} }
async updateLastLaunchedDate(id: string): Promise<void> { async updateLastLaunchedDate(id: string, userId: UserId): Promise<void> {
const userId = await firstValueFrom(this.stateProvider.activeUserId$); let ciphersLocalData = await firstValueFrom(this.localData$(userId));
let ciphersLocalData = await firstValueFrom(this.localData$);
if (!ciphersLocalData) { if (!ciphersLocalData) {
ciphersLocalData = {}; ciphersLocalData = {};
@@ -707,9 +706,9 @@ export class CipherService implements CipherServiceAbstraction {
lastUsedDate: currentTime, 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) { if (!decryptedCipherCache) {
return; return;
} }
@@ -914,13 +913,13 @@ export class CipherService implements CipherServiceAbstraction {
return new Cipher(cData); return new Cipher(cData);
} }
async saveCollectionsWithServer(cipher: Cipher): Promise<Cipher> { async saveCollectionsWithServer(cipher: Cipher, userId: UserId): Promise<Cipher> {
const request = new CipherCollectionsRequest(cipher.collectionIds); const request = new CipherCollectionsRequest(cipher.collectionIds);
const response = await this.apiService.putCipherCollections(cipher.id, request); const response = await this.apiService.putCipherCollections(cipher.id, request);
// The response will now check for an unavailable value. This value determines whether // 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. // the user still has Can Manage access to the item after updating.
if (response.unavailable) { if (response.unavailable) {
await this.delete(cipher.id); await this.delete(cipher.id, userId);
return; return;
} }
const data = new CipherData(response.cipher); const data = new CipherData(response.cipher);
@@ -944,6 +943,7 @@ export class CipherService implements CipherServiceAbstraction {
*/ */
async bulkUpdateCollectionsWithServer( async bulkUpdateCollectionsWithServer(
orgId: OrganizationId, orgId: OrganizationId,
userId: UserId,
cipherIds: CipherId[], cipherIds: CipherId[],
collectionIds: CollectionId[], collectionIds: CollectionId[],
removeCollections: boolean = false, removeCollections: boolean = false,
@@ -958,7 +958,7 @@ export class CipherService implements CipherServiceAbstraction {
await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false); await this.apiService.send("POST", "/ciphers/bulk-collections", request, true, false);
// Update the local state // Update the local state
const ciphers = await firstValueFrom(this.ciphers$); const ciphers = await firstValueFrom(this.ciphers$(userId));
for (const id of cipherIds) { for (const id of cipherIds) {
const cipher = ciphers[id]; const cipher = ciphers[id];
@@ -975,7 +975,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => ciphers); await this.encryptedCiphersState(userId).update(() => ciphers);
} }
async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> { async upsert(cipher: CipherData | CipherData[]): Promise<Record<CipherId, CipherData>> {
@@ -1016,10 +1016,10 @@ export class CipherService implements CipherServiceAbstraction {
await this.clearCache(userId); await this.clearCache(userId);
} }
async moveManyWithServer(ids: string[], folderId: string): Promise<any> { async moveManyWithServer(ids: string[], folderId: string, userId: UserId): Promise<any> {
await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId)); await this.apiService.putMoveCiphers(new CipherBulkMoveRequest(ids, folderId));
let ciphers = await firstValueFrom(this.ciphers$); let ciphers = await firstValueFrom(this.ciphers$(userId));
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -1032,11 +1032,11 @@ export class CipherService implements CipherServiceAbstraction {
}); });
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => ciphers); await this.encryptedCiphersState(userId).update(() => ciphers);
} }
async delete(id: string | string[]): Promise<any> { async delete(id: string | string[], userId: UserId): Promise<any> {
const ciphers = await firstValueFrom(this.ciphers$); const ciphers = await firstValueFrom(this.ciphers$(userId));
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
@@ -1054,35 +1054,36 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => ciphers); await this.encryptedCiphersState(userId).update(() => ciphers);
} }
async deleteWithServer(id: string, asAdmin = false): Promise<any> { async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
if (asAdmin) { if (asAdmin) {
await this.apiService.deleteCipherAdmin(id); await this.apiService.deleteCipherAdmin(id);
} else { } else {
await this.apiService.deleteCipher(id); await this.apiService.deleteCipher(id);
} }
await this.delete(id); await this.delete(id, userId);
} }
async deleteManyWithServer(ids: string[], asAdmin = false): Promise<any> { async deleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise<any> {
const request = new CipherBulkDeleteRequest(ids); const request = new CipherBulkDeleteRequest(ids);
if (asAdmin) { if (asAdmin) {
await this.apiService.deleteManyCiphersAdmin(request); await this.apiService.deleteManyCiphersAdmin(request);
} else { } else {
await this.apiService.deleteManyCiphers(request); await this.apiService.deleteManyCiphers(request);
} }
await this.delete(ids); await this.delete(ids, userId);
} }
async deleteAttachment( async deleteAttachment(
id: string, id: string,
revisionDate: string, revisionDate: string,
attachmentId: string, attachmentId: string,
userId: UserId,
): Promise<CipherData> { ): Promise<CipherData> {
let ciphers = await firstValueFrom(this.ciphers$); let ciphers = await firstValueFrom(this.ciphers$(userId));
const cipherId = id as CipherId; const cipherId = id as CipherId;
// eslint-disable-next-line // eslint-disable-next-line
if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) { if (ciphers == null || !ciphers.hasOwnProperty(id) || ciphers[cipherId].attachments == null) {
@@ -1100,7 +1101,7 @@ export class CipherService implements CipherServiceAbstraction {
ciphers[cipherId].revisionDate = revisionDate; ciphers[cipherId].revisionDate = revisionDate;
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => { await this.encryptedCiphersState(userId).update(() => {
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -1110,7 +1111,11 @@ export class CipherService implements CipherServiceAbstraction {
return ciphers[cipherId]; return ciphers[cipherId];
} }
async deleteAttachmentWithServer(id: string, attachmentId: string): Promise<CipherData> { async deleteAttachmentWithServer(
id: string,
attachmentId: string,
userId: UserId,
): Promise<CipherData> {
let cipherResponse = null; let cipherResponse = null;
try { try {
cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId); cipherResponse = await this.apiService.deleteCipherAttachment(id, attachmentId);
@@ -1119,7 +1124,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
const cipherData = CipherData.fromJSON(cipherResponse?.cipher); 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 { sortCiphersByLastUsed(a: CipherView, b: CipherView): number {
@@ -1192,8 +1197,8 @@ export class CipherService implements CipherServiceAbstraction {
}; };
} }
async softDelete(id: string | string[]): Promise<any> { async softDelete(id: string | string[], userId: UserId): Promise<any> {
let ciphers = await firstValueFrom(this.ciphers$); let ciphers = await firstValueFrom(this.ciphers$(userId));
if (ciphers == null) { if (ciphers == null) {
return; return;
} }
@@ -1212,7 +1217,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => { await this.encryptedCiphersState(userId).update(() => {
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -1220,17 +1225,17 @@ export class CipherService implements CipherServiceAbstraction {
}); });
} }
async softDeleteWithServer(id: string, asAdmin = false): Promise<any> { async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
if (asAdmin) { if (asAdmin) {
await this.apiService.putDeleteCipherAdmin(id); await this.apiService.putDeleteCipherAdmin(id);
} else { } else {
await this.apiService.putDeleteCipher(id); await this.apiService.putDeleteCipher(id);
} }
await this.softDelete(id); await this.softDelete(id, userId);
} }
async softDeleteManyWithServer(ids: string[], asAdmin = false): Promise<any> { async softDeleteManyWithServer(ids: string[], userId: UserId, asAdmin = false): Promise<any> {
const request = new CipherBulkDeleteRequest(ids); const request = new CipherBulkDeleteRequest(ids);
if (asAdmin) { if (asAdmin) {
await this.apiService.putDeleteManyCiphersAdmin(request); await this.apiService.putDeleteManyCiphersAdmin(request);
@@ -1238,13 +1243,14 @@ export class CipherService implements CipherServiceAbstraction {
await this.apiService.putDeleteManyCiphers(request); await this.apiService.putDeleteManyCiphers(request);
} }
await this.softDelete(ids); await this.softDelete(ids, userId);
} }
async restore( async restore(
cipher: { id: string; revisionDate: string } | { id: string; revisionDate: string }[], 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) { if (ciphers == null) {
return; return;
} }
@@ -1265,7 +1271,7 @@ export class CipherService implements CipherServiceAbstraction {
} }
await this.clearCache(); await this.clearCache();
await this.encryptedCiphersState.update(() => { await this.encryptedCiphersState(userId).update(() => {
if (ciphers == null) { if (ciphers == null) {
ciphers = {}; ciphers = {};
} }
@@ -1273,7 +1279,7 @@ export class CipherService implements CipherServiceAbstraction {
}); });
} }
async restoreWithServer(id: string, asAdmin = false): Promise<any> { async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise<any> {
let response; let response;
if (asAdmin) { if (asAdmin) {
response = await this.apiService.putRestoreCipherAdmin(id); response = await this.apiService.putRestoreCipherAdmin(id);
@@ -1281,14 +1287,14 @@ export class CipherService implements CipherServiceAbstraction {
response = await this.apiService.putRestoreCipher(id); 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 * 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 * 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<void> { async restoreManyWithServer(ids: string[], userId: UserId, orgId: string = null): Promise<void> {
let response; let response;
if (orgId) { if (orgId) {
@@ -1303,7 +1309,7 @@ export class CipherService implements CipherServiceAbstraction {
for (const cipher of response.data) { for (const cipher of response.data) {
restores.push({ id: cipher.id, revisionDate: cipher.revisionDate }); restores.push({ id: cipher.id, revisionDate: cipher.revisionDate });
} }
await this.restore(restores); await this.restore(restores, userId);
} }
async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> { async getKeyForCipherKeyDecryption(cipher: Cipher, userId: UserId): Promise<UserKey | OrgKey> {
@@ -1313,8 +1319,8 @@ export class CipherService implements CipherServiceAbstraction {
); );
} }
async setAddEditCipherInfo(value: AddEditCipherInfo) { async setAddEditCipherInfo(value: AddEditCipherInfo, userId: UserId) {
await this.addEditCipherInfoState.update(() => value, { await this.addEditCipherInfoState(userId).update(() => value, {
shouldUpdate: (current) => !(current == null && value == null), shouldUpdate: (current) => !(current == null && value == null),
}); });
} }
@@ -1333,8 +1339,8 @@ export class CipherService implements CipherServiceAbstraction {
let encryptedCiphers: CipherWithIdRequest[] = []; let encryptedCiphers: CipherWithIdRequest[] = [];
const ciphers = await firstValueFrom(this.cipherViews$); const ciphers = await firstValueFrom(this.cipherViews$(userId));
const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$); const failedCiphers = await firstValueFrom(this.failedToDecryptCiphers$(userId));
if (!ciphers) { if (!ciphers) {
return encryptedCiphers; return encryptedCiphers;
} }
@@ -1357,6 +1363,41 @@ export class CipherService implements CipherServiceAbstraction {
return encryptedCiphers; 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 // Helpers
// In the case of a cipher that is being shared with an organization, we want to decrypt the // 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( private async getCipherForUrl(
url: string, url: string,
userId: UserId,
lastUsed: boolean, lastUsed: boolean,
lastLaunched: boolean, lastLaunched: boolean,
autofillOnPageLoad: boolean, autofillOnPageLoad: boolean,
@@ -1667,7 +1709,7 @@ export class CipherService implements CipherServiceAbstraction {
const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url; const cacheKey = autofillOnPageLoad ? "autofillOnPageLoad-" + url : url;
if (!this.sortedCiphersCache.isCached(cacheKey)) { if (!this.sortedCiphersCache.isCached(cacheKey)) {
let ciphers = await this.getAllDecryptedForUrl(url); let ciphers = await this.getAllDecryptedForUrl(url, userId);
if (!ciphers) { if (!ciphers) {
return null; return null;
} }

View File

@@ -190,7 +190,7 @@ export class FolderService implements InternalFolderServiceAbstraction {
}); });
// Items in a deleted folder are re-assigned to "No Folder" // 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) { if (ciphers != null) {
const updates: Cipher[] = []; const updates: Cipher[] = [];
for (const cId in ciphers) { for (const cId in ciphers) {

View File

@@ -103,7 +103,7 @@ export class DefaultUserAsymmetricKeysRegenerationService
} }
// The private isn't decryptable, check to see if we can decrypt something with the userKey. // 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) { if (userKeyCanDecrypt) {
this.logService.info( this.logService.info(
"[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.", "[UserAsymmetricKeyRegeneration] User Asymmetric Key decryption failure detected, attempting regeneration.",
@@ -155,8 +155,8 @@ export class DefaultUserAsymmetricKeysRegenerationService
); );
} }
private async userKeyCanDecrypt(userKey: UserKey): Promise<boolean> { private async userKeyCanDecrypt(userKey: UserKey, userId: UserId): Promise<boolean> {
const ciphers = await this.cipherService.getAll(); const ciphers = await this.cipherService.getAll(userId);
const cipher = ciphers.find((cipher) => cipher.organizationId == null); const cipher = ciphers.find((cipher) => cipher.organizationId == null);
if (cipher != null) { if (cipher != null) {

View File

@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import * as papa from "papaparse"; import * as papa from "papaparse";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export"; import { CipherWithIdExport, FolderWithIdExport } from "@bitwarden/common/models/export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -32,8 +33,6 @@ export class IndividualVaultExportService
extends BaseVaultExportService extends BaseVaultExportService
implements IndividualVaultExportServiceAbstraction implements IndividualVaultExportServiceAbstraction
{ {
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
constructor( constructor(
private folderService: FolderService, private folderService: FolderService,
private cipherService: CipherService, private cipherService: CipherService,
@@ -63,7 +62,7 @@ export class IndividualVaultExportService
let decFolders: FolderView[] = []; let decFolders: FolderView[] = [];
let decCiphers: CipherView[] = []; let decCiphers: CipherView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => { firstValueFrom(this.folderService.folderViews$(activeUserId)).then((folders) => {
@@ -72,7 +71,7 @@ export class IndividualVaultExportService
); );
promises.push( promises.push(
this.cipherService.getAllDecrypted().then((ciphers) => { this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => {
decCiphers = ciphers.filter((f) => f.deletedDate == null); decCiphers = ciphers.filter((f) => f.deletedDate == null);
}), }),
); );
@@ -90,7 +89,7 @@ export class IndividualVaultExportService
let folders: Folder[] = []; let folders: Folder[] = [];
let ciphers: Cipher[] = []; let ciphers: Cipher[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => { firstValueFrom(this.folderService.folders$(activeUserId)).then((f) => {
@@ -99,7 +98,7 @@ export class IndividualVaultExportService
); );
promises.push( promises.push(
this.cipherService.getAll().then((c) => { this.cipherService.getAll(activeUserId).then((c) => {
ciphers = c.filter((f) => f.deletedDate == null); ciphers = c.filter((f) => f.deletedDate == null);
}), }),
); );
@@ -107,7 +106,7 @@ export class IndividualVaultExportService
await Promise.all(promises); await Promise.all(promises);
const userKey = await this.keyService.getUserKeyWithLegacySupport( 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); const encKeyValidation = await this.encryptService.encrypt(Utils.newGuid(), userKey);

View File

@@ -1,7 +1,7 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import * as papa from "papaparse"; import * as papa from "papaparse";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { import {
CollectionService, CollectionService,
@@ -13,6 +13,7 @@ import {
import { PinServiceAbstraction } from "@bitwarden/auth/common"; import { PinServiceAbstraction } from "@bitwarden/auth/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export"; import { CipherWithIdExport, CollectionWithIdExport } from "@bitwarden/common/models/export";
import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service"; import { CryptoFunctionService } from "@bitwarden/common/platform/abstractions/crypto-function.service";
@@ -94,9 +95,7 @@ export class OrganizationVaultExportService
const decCollections: CollectionView[] = []; const decCollections: CollectionView[] = [];
const decCiphers: CipherView[] = []; const decCiphers: CipherView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
promises.push( promises.push(
this.apiService.getOrganizationExport(organizationId).then((exportData) => { this.apiService.getOrganizationExport(organizationId).then((exportData) => {
@@ -184,6 +183,7 @@ export class OrganizationVaultExportService
let allDecCiphers: CipherView[] = []; let allDecCiphers: CipherView[] = [];
let decCollections: CollectionView[] = []; let decCollections: CollectionView[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
this.collectionService.getAllDecrypted().then(async (collections) => { this.collectionService.getAllDecrypted().then(async (collections) => {
@@ -192,7 +192,7 @@ export class OrganizationVaultExportService
); );
promises.push( promises.push(
this.cipherService.getAllDecrypted().then((ciphers) => { this.cipherService.getAllDecrypted(activeUserId).then((ciphers) => {
allDecCiphers = ciphers; allDecCiphers = ciphers;
}), }),
); );
@@ -216,6 +216,7 @@ export class OrganizationVaultExportService
let allCiphers: Cipher[] = []; let allCiphers: Cipher[] = [];
let encCollections: Collection[] = []; let encCollections: Collection[] = [];
const promises = []; const promises = [];
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
promises.push( promises.push(
this.collectionService.getAll().then((collections) => { this.collectionService.getAll().then((collections) => {
@@ -224,7 +225,7 @@ export class OrganizationVaultExportService
); );
promises.push( promises.push(
this.cipherService.getAll().then((ciphers) => { this.cipherService.getAll(activeUserId).then((ciphers) => {
allCiphers = ciphers; allCiphers = ciphers;
}), }),
); );

View File

@@ -110,7 +110,7 @@ describe("CipherAttachmentsComponent", () => {
it("fetches cipherView using `cipherId`", async () => { it("fetches cipherView using `cipherId`", async () => {
await component.ngOnInit(); await component.ngOnInit();
expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333"); expect(cipherServiceGet).toHaveBeenCalledWith("5555-444-3333", mockUserId);
expect(component.cipher).toEqual(cipherView); expect(component.cipher).toEqual(cipherView);
}); });

View File

@@ -21,10 +21,11 @@ import {
ReactiveFormsModule, ReactiveFormsModule,
Validators, Validators,
} from "@angular/forms"; } from "@angular/forms";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; import { JslibModule } from "@bitwarden/angular/jslib.module";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherId, UserId } from "@bitwarden/common/types/guid";
@@ -118,10 +119,8 @@ export class CipherAttachmentsComponent implements OnInit, AfterViewInit {
} }
async ngOnInit(): Promise<void> { async ngOnInit(): Promise<void> {
this.cipherDomain = await this.cipherService.get(this.cipherId); this.activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.activeUserId = await firstValueFrom( this.cipherDomain = await this.cipherService.get(this.cipherId, this.activeUserId);
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
this.cipher = await this.cipherDomain.decrypt( this.cipher = await this.cipherDomain.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, this.activeUserId),
); );

View File

@@ -2,12 +2,16 @@ import { ComponentFixture, TestBed } from "@angular/core/testing";
import { By } from "@angular/platform-browser"; import { By } from "@angular/platform-browser";
import { mock } from "jest-mock-extended"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view"; import { AttachmentView } from "@bitwarden/common/vault/models/view/attachment.view";
import { DialogService, ToastService } from "@bitwarden/components"; import { DialogService, ToastService } from "@bitwarden/components";
import { mockAccountServiceWith } from "../../../../../../common/spec";
import { DeleteAttachmentComponent } from "./delete-attachment.component"; import { DeleteAttachmentComponent } from "./delete-attachment.component";
describe("DeleteAttachmentComponent", () => { describe("DeleteAttachmentComponent", () => {
@@ -42,6 +46,7 @@ describe("DeleteAttachmentComponent", () => {
}, },
{ provide: I18nService, useValue: { t: (key: string) => key } }, { provide: I18nService, useValue: { t: (key: string) => key } },
{ provide: LogService, useValue: mock<LogService>() }, { provide: LogService, useValue: mock<LogService>() },
{ provide: AccountService, useValue: mockAccountServiceWith("UserId" as UserId) },
], ],
}) })
.overrideProvider(DialogService, { .overrideProvider(DialogService, {
@@ -90,7 +95,11 @@ describe("DeleteAttachmentComponent", () => {
}); });
// Called with cipher id and attachment id // 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 () => { it("shows toast message on successful deletion", async () => {

View File

@@ -1,7 +1,10 @@
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, EventEmitter, Input, Output } from "@angular/core"; import { Component, EventEmitter, Input, Output } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
@@ -36,6 +39,7 @@ export class DeleteAttachmentComponent {
private cipherService: CipherService, private cipherService: CipherService,
private logService: LogService, private logService: LogService,
private dialogService: DialogService, private dialogService: DialogService,
private accountService: AccountService,
) {} ) {}
delete = async () => { delete = async () => {
@@ -50,7 +54,19 @@ export class DeleteAttachmentComponent {
} }
try { 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({ this.toastService.showToast({
variant: "success", variant: "success",

View File

@@ -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 { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; 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 { CipherId, UserId } from "@bitwarden/common/types/guid";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; 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 collectionService: CollectionService = inject(CollectionService);
private accountService = inject(AccountService); private accountService = inject(AccountService);
private activeUserId$ = this.accountService.activeAccount$.pipe(map((a) => a?.id));
async buildConfig( async buildConfig(
mode: CipherFormMode, mode: CipherFormMode,
cipherId?: CipherId, cipherId?: CipherId,
cipherType?: CipherType, cipherType?: CipherType,
): Promise<CipherFormConfig> { ): Promise<CipherFormConfig> {
const activeUserId = await firstValueFrom(this.activeUserId$); const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const [organizations, collections, allowPersonalOwnership, folders, cipher] = const [organizations, collections, allowPersonalOwnership, folders, cipher] =
await firstValueFrom( 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) .policyAppliesToActiveUser$(PolicyType.PersonalOwnership)
.pipe(map((p) => !p)); .pipe(map((p) => !p));
private getCipher(id?: CipherId): Promise<Cipher | null> { private getCipher(userId: UserId, id?: CipherId): Promise<Cipher | null> {
if (id == null) { if (id == null) {
return Promise.resolve(null); return Promise.resolve(null);
} }
return this.cipherService.get(id); return this.cipherService.get(id, userId);
} }
} }

View File

@@ -1,10 +1,11 @@
// FIXME: Update this file to be type safe and remove this and next line // FIXME: Update this file to be type safe and remove this and next line
// @ts-strict-ignore // @ts-strict-ignore
import { inject, Injectable } from "@angular/core"; import { inject, Injectable } from "@angular/core";
import { firstValueFrom, map } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.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 { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { Cipher } from "@bitwarden/common/vault/models/domain/cipher";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
@@ -23,9 +24,7 @@ export class DefaultCipherFormService implements CipherFormService {
private apiService: ApiService = inject(ApiService); private apiService: ApiService = inject(ApiService);
async decryptCipher(cipher: Cipher): Promise<CipherView> { async decryptCipher(cipher: Cipher): Promise<CipherView> {
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
return await cipher.decrypt( return await cipher.decrypt(
await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId),
); );
@@ -33,9 +32,7 @@ export class DefaultCipherFormService implements CipherFormService {
async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> { async saveCipher(cipher: CipherView, config: CipherFormConfig): Promise<CipherView> {
// Passing the original cipher is important here as it is responsible for appending to password history // Passing the original cipher is important here as it is responsible for appending to password history
const activeUserId = await firstValueFrom( const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const encryptedCipher = await this.cipherService.encrypt( const encryptedCipher = await this.cipherService.encrypt(
cipher, cipher,
activeUserId, 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 // When using an admin config or the cipher was unassigned, update collections as an admin
savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher); savedCipher = await this.cipherService.saveCollectionsWithServerAdmin(encryptedCipher);
} else { } else {
savedCipher = await this.cipherService.saveCollectionsWithServer(encryptedCipher); savedCipher = await this.cipherService.saveCollectionsWithServer(
encryptedCipher,
activeUserId,
);
} }
} }

View File

@@ -2,8 +2,11 @@
// @ts-strict-ignore // @ts-strict-ignore
import { CommonModule } from "@angular/common"; import { CommonModule } from "@angular/common";
import { Component, Input } from "@angular/core"; import { Component, Input } from "@angular/core";
import { firstValueFrom } from "rxjs";
import { JslibModule } from "@bitwarden/angular/jslib.module"; 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 { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view"; import { LoginUriView } from "@bitwarden/common/vault/models/view/login-uri.view";
@@ -38,10 +41,12 @@ export class AutofillOptionsViewComponent {
constructor( constructor(
private platformUtilsService: PlatformUtilsService, private platformUtilsService: PlatformUtilsService,
private cipherService: CipherService, private cipherService: CipherService,
private accountService: AccountService,
) {} ) {}
async openWebsite(selectedUri: string) { 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); this.platformUtilsService.launchUri(selectedUri);
} }
} }

View File

@@ -179,7 +179,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
private get selectedOrgId(): OrganizationId { private get selectedOrgId(): OrganizationId {
return this.formGroup.getRawValue().selectedOrg || this.params.organizationId; return this.formGroup.getRawValue().selectedOrg || this.params.organizationId;
} }
private activeUserId: UserId;
private destroy$ = new Subject<void>(); private destroy$ = new Subject<void>();
constructor( constructor(
@@ -193,10 +192,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
) {} ) {}
async ngOnInit() { async ngOnInit() {
this.activeUserId = await firstValueFrom(
this.accountService.activeAccount$.pipe(map((a) => a?.id)),
);
const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null); const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null);
if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) { if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) {
@@ -253,12 +248,15 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
.filter((i) => i.organizationId) .filter((i) => i.organizationId)
.map((i) => i.id as CipherId); .map((i) => i.id as CipherId);
const activeUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
// Move personal items to the organization // Move personal items to the organization
if (this.personalItemsCount > 0) { if (this.personalItemsCount > 0) {
await this.moveToOrganization( await this.moveToOrganization(
this.selectedOrgId, this.selectedOrgId,
this.params.ciphers.filter((c) => c.organizationId == null), this.params.ciphers.filter((c) => c.organizationId == null),
this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), 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 // Update assigned collections for single org cipher or bulk update collections for multiple org ciphers
await (isSingleOrgCipher await (isSingleOrgCipher
? this.updateAssignedCollections(this.editableItems[0]) ? this.updateAssignedCollections(this.editableItems[0], activeUserId)
: this.bulkUpdateCollections(cipherIds)); : this.bulkUpdateCollections(cipherIds, activeUserId));
this.toastService.showToast({ this.toastService.showToast({
variant: "success", variant: "success",
@@ -447,12 +445,13 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
organizationId: OrganizationId, organizationId: OrganizationId,
shareableCiphers: CipherView[], shareableCiphers: CipherView[],
selectedCollectionIds: CollectionId[], selectedCollectionIds: CollectionId[],
userId: UserId,
) { ) {
await this.cipherService.shareManyWithServer( await this.cipherService.shareManyWithServer(
shareableCiphers, shareableCiphers,
organizationId, organizationId,
selectedCollectionIds, selectedCollectionIds,
this.activeUserId, userId,
); );
this.toastService.showToast({ 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) { if (this.formGroup.controls.collections.value.length > 0) {
await this.cipherService.bulkUpdateCollectionsWithServer( await this.cipherService.bulkUpdateCollectionsWithServer(
this.selectedOrgId, this.selectedOrgId,
userId,
cipherIds, cipherIds,
this.formGroup.controls.collections.value.map((i) => i.id as CollectionId), this.formGroup.controls.collections.value.map((i) => i.id as CollectionId),
false, false,
@@ -483,6 +483,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI
) { ) {
await this.cipherService.bulkUpdateCollectionsWithServer( await this.cipherService.bulkUpdateCollectionsWithServer(
this.selectedOrgId, this.selectedOrgId,
userId,
cipherIds, cipherIds,
[this.params.activeCollection.id as CollectionId], [this.params.activeCollection.id as CollectionId],
true, 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(); const { collections } = this.formGroup.getRawValue();
cipherView.collectionIds = collections.map((i) => i.id as CollectionId); 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) { if (this.params.isSingleCipherAdmin) {
await this.cipherService.saveCollectionsWithServerAdmin(cipher); await this.cipherService.saveCollectionsWithServerAdmin(cipher);
} else { } else {
await this.cipherService.saveCollectionsWithServer(cipher); await this.cipherService.saveCollectionsWithServer(cipher, userId);
} }
} }
} }