diff --git a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts index 202103c16d..c10236236e 100644 --- a/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts +++ b/apps/browser/src/vault/popup/components/vault-v2/assign-collections/assign-collections.component.ts @@ -8,6 +8,7 @@ import { Observable, combineLatest, first, map, switchMap } from "rxjs"; import { CollectionService } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; @@ -49,6 +50,8 @@ export class AssignCollections { /** Params needed to populate the assign collections component */ params: CollectionAssignmentParams; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( private location: Location, private collectionService: CollectionService, @@ -70,7 +73,7 @@ export class AssignCollections { ), ); - combineLatest([cipher$, this.collectionService.decryptedCollections$]) + combineLatest([cipher$, this.collectionService.decryptedCollections$(this.activeUserId$)]) .pipe(takeUntilDestroyed(), first()) .subscribe(([cipherView, collections]) => { let availableCollections = collections.filter((c) => !c.readOnly); diff --git a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts index ee6858fe44..614eaafe1b 100644 --- a/apps/browser/src/vault/popup/components/vault/vault-items.component.ts +++ b/apps/browser/src/vault/popup/components/vault/vault-items.component.ts @@ -22,6 +22,9 @@ import { BrowserApi } from "../../../../platform/browser/browser-api"; import BrowserPopupUtils from "../../../../platform/popup/browser-popup-utils"; import { VaultBrowserStateService } from "../../../services/vault-browser-state.service"; import { VaultFilterService } from "../../../services/vault-filter.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { firstValueFrom } from "rxjs"; const ComponentId = "VaultItemsComponent"; @@ -49,6 +52,8 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn private applySavedState = true; private scrollingContainer = "cdk-virtual-scroll-viewport"; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( searchService: SearchService, private organizationService: OrganizationService, @@ -64,6 +69,7 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn private platformUtilsService: PlatformUtilsService, cipherService: CipherService, private vaultFilterService: VaultFilterService, + private accountService: AccountService, ) { super(searchService, cipherService); this.applySavedState = @@ -133,7 +139,10 @@ export class VaultItemsComponent extends BaseVaultItemsComponent implements OnIn this.showVaultFilter = false; this.collectionId = params.collectionId; this.searchPlaceholder = this.i18nService.t("searchCollection"); - const collectionNode = await this.collectionService.getNested(this.collectionId); + const allCollections = await firstValueFrom( + this.collectionService.decryptedCollections$(this.activeUserId$), + ); + const collectionNode = this.collectionService.getNested(allCollections, this.collectionId); if (collectionNode != null && collectionNode.node != null) { this.groupingTitle = collectionNode.node.name; this.nestedCollections = diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts index 42d76e1dfe..72189b306d 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.spec.ts @@ -8,8 +8,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { SyncService } from "@bitwarden/common/platform/sync"; -import { ObservableTracker } from "@bitwarden/common/spec"; -import { CipherId } from "@bitwarden/common/types/guid"; +import { mockAccountServiceWith, ObservableTracker } from "@bitwarden/common/spec"; +import { CipherId, UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { VaultSettingsService } from "@bitwarden/common/vault/abstractions/vault-settings/vault-settings.service"; import { CipherType } from "@bitwarden/common/vault/enums"; @@ -20,6 +20,7 @@ import { BrowserApi } from "../../../platform/browser/browser-api"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; import { VaultPopupItemsService } from "./vault-popup-items.service"; import { VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; describe("VaultPopupItemsService", () => { let testBed: TestBed; @@ -39,6 +40,7 @@ describe("VaultPopupItemsService", () => { const collectionService = mock(); const vaultAutofillServiceMock = mock(); const syncServiceMock = mock(); + const accountServiceMock = mockAccountServiceWith("UserId" as UserId); beforeEach(() => { allCiphers = cipherFactory(10); @@ -90,7 +92,7 @@ describe("VaultPopupItemsService", () => { ]; organizationServiceMock.organizations$ = new BehaviorSubject([mockOrg]); - collectionService.decryptedCollections$ = new BehaviorSubject(mockCollections); + collectionService.decryptedCollections$.mockReturnValue(new BehaviorSubject(mockCollections)); activeUserLastSync$ = new BehaviorSubject(new Date()); syncServiceMock.activeUserLastSync$.mockReturnValue(activeUserLastSync$); @@ -105,6 +107,7 @@ describe("VaultPopupItemsService", () => { { provide: CollectionService, useValue: collectionService }, { provide: VaultPopupAutofillService, useValue: vaultAutofillServiceMock }, { provide: SyncService, useValue: syncServiceMock }, + { provide: AccountService, useValue: accountServiceMock }, ], }); diff --git a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts index 09c7d5fb0d..848302949c 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-items.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-items.service.ts @@ -36,6 +36,8 @@ import { PopupCipherView } from "../views/popup-cipher.view"; import { VaultPopupAutofillService } from "./vault-popup-autofill.service"; import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-filters.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; /** * Service for managing the various item lists on the new Vault tab in the browser popup. @@ -45,6 +47,7 @@ import { MY_VAULT_ID, VaultPopupListFiltersService } from "./vault-popup-list-fi }) export class VaultPopupItemsService { private _searchText$ = new BehaviorSubject(""); + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); /** * Subject that emits whenever new ciphers are being processed/filtered. @@ -90,7 +93,7 @@ export class VaultPopupItemsService { switchMap((ciphers) => combineLatest([ this.organizationService.organizations$, - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(this.activeUserId$), ]).pipe( map(([organizations, collections]) => { const orgMap = Object.fromEntries(organizations.map((org) => [org.id, org])); @@ -260,6 +263,7 @@ export class VaultPopupItemsService { private collectionService: CollectionService, private vaultPopupAutofillService: VaultPopupAutofillService, private syncService: SyncService, + private accountService: AccountService, ) {} applyFilter(newSearchText: string) { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts index 5d7e690193..1695a03788 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.spec.ts @@ -7,11 +7,15 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; +import { mockAccountServiceWith } from "@bitwarden/common/spec"; +import { UserId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherType } from "@bitwarden/common/vault/enums"; +import { TreeNode } from "@bitwarden/common/vault/models/domain/tree-node"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -26,8 +30,8 @@ describe("VaultPopupListFiltersService", () => { const policyAppliesToActiveUser$ = new BehaviorSubject(false); const collectionService = { - decryptedCollections$, - getAllNested: () => Promise.resolve([]), + decryptedCollections$: () => decryptedCollections$, + getAllNested: () => [] as TreeNode[], } as unknown as CollectionService; const folderService = { @@ -50,13 +54,15 @@ describe("VaultPopupListFiltersService", () => { policyAppliesToActiveUser$: jest.fn(() => policyAppliesToActiveUser$), }; + const accountService = mockAccountServiceWith("userId" as UserId); + beforeEach(() => { memberOrganizations$.next([]); decryptedCollections$.next([]); policyAppliesToActiveUser$.next(false); policyService.policyAppliesToActiveUser$.mockClear(); - collectionService.getAllNested = () => Promise.resolve([]); + collectionService.getAllNested = () => [] as TreeNode[]; TestBed.configureTestingModule({ providers: [ { @@ -84,6 +90,10 @@ describe("VaultPopupListFiltersService", () => { useValue: policyService, }, { provide: FormBuilder, useClass: FormBuilder }, + { + provide: AccountService, + useValue: accountService, + }, ], }); @@ -276,13 +286,11 @@ describe("VaultPopupListFiltersService", () => { decryptedCollections$.next(testCollections); collectionService.getAllNested = () => - Promise.resolve( - testCollections.map((c) => ({ - children: [], - node: c, - parent: null, - })), - ); + testCollections.map((c) => ({ + children: [], + node: c, + parent: null, + })); }); it("returns all collections", (done) => { diff --git a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts index 4059a43b56..bcb720f170 100644 --- a/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts +++ b/apps/browser/src/vault/popup/services/vault-popup-list-filters.service.ts @@ -1,15 +1,7 @@ import { Injectable } from "@angular/core"; import { takeUntilDestroyed } from "@angular/core/rxjs-interop"; import { FormBuilder } from "@angular/forms"; -import { - combineLatest, - distinctUntilChanged, - map, - Observable, - startWith, - switchMap, - tap, -} from "rxjs"; +import { combineLatest, distinctUntilChanged, map, Observable, startWith, tap } from "rxjs"; import { CollectionService, Collection, CollectionView } from "@bitwarden/admin-console/common"; import { DynamicTreeNode } from "@bitwarden/angular/vault/vault-filter/models/dynamic-tree-node.model"; @@ -17,6 +9,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ProductTierType } from "@bitwarden/common/billing/enums"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; @@ -81,6 +75,8 @@ export class VaultPopupListFiltersService { map((ciphers) => Object.values(ciphers)), ); + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( private folderService: FolderService, private cipherService: CipherService, @@ -89,6 +85,7 @@ export class VaultPopupListFiltersService { private collectionService: CollectionService, private formBuilder: FormBuilder, private policyService: PolicyService, + private accountService: AccountService, ) { this.filterForm.controls.organization.valueChanges .pipe(takeUntilDestroyed()) @@ -302,7 +299,7 @@ export class VaultPopupListFiltersService { previousFilter.organization?.id === currentFilter.organization?.id, ), ), - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(this.activeUserId$), ]).pipe( map(([filters, allCollections]) => { const organizationId = filters.organization?.id ?? null; @@ -314,8 +311,8 @@ export class VaultPopupListFiltersService { return collections; }), - switchMap(async (collections) => { - const nestedCollections = await this.collectionService.getAllNested(collections); + map((collections) => { + const nestedCollections = this.collectionService.getAllNested(collections); return new DynamicTreeNode({ fullList: collections, diff --git a/apps/browser/src/vault/services/vault-filter.service.ts b/apps/browser/src/vault/services/vault-filter.service.ts index 50858076d7..2e2ea3ad74 100644 --- a/apps/browser/src/vault/services/vault-filter.service.ts +++ b/apps/browser/src/vault/services/vault-filter.service.ts @@ -22,7 +22,7 @@ export class VaultFilterService extends BaseVaultFilterService { collectionService: CollectionService, policyService: PolicyService, stateProvider: StateProvider, - private accountService: AccountService, + accountService: AccountService, ) { super( organizationService, @@ -31,6 +31,7 @@ export class VaultFilterService extends BaseVaultFilterService { collectionService, policyService, stateProvider, + accountService, ); this.vaultFilter.myVaultOnly = false; this.vaultFilter.selectedOrganizationId = null; diff --git a/libs/angular/src/admin-console/components/collections.component.ts b/libs/angular/src/admin-console/components/collections.component.ts index 304ff4411c..bc3c1fad46 100644 --- a/libs/angular/src/admin-console/components/collections.component.ts +++ b/libs/angular/src/admin-console/components/collections.component.ts @@ -1,10 +1,11 @@ import { Directive, EventEmitter, Input, OnInit, Output } from "@angular/core"; -import { firstValueFrom, map } from "rxjs"; +import { firstValueFrom } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -25,6 +26,8 @@ export class CollectionsComponent implements OnInit { collections: CollectionView[] = []; organization: Organization; + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + protected cipherDomain: Cipher; constructor( @@ -45,9 +48,7 @@ export class CollectionsComponent implements OnInit { async load() { this.cipherDomain = await this.loadCipher(); this.collectionIds = this.loadCipherCollections(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); this.cipher = await this.cipherDomain.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(this.cipherDomain, activeUserId), ); @@ -113,7 +114,9 @@ export class CollectionsComponent implements OnInit { } protected async loadCollections() { - const allCollections = await this.collectionService.getAllDecrypted(); + const allCollections = await firstValueFrom( + this.collectionService.decryptedCollections$(this.activeUserId$), + ); return allCollections.filter( (c) => !c.readOnly && c.organizationId === this.cipher.organizationId, ); diff --git a/libs/angular/src/components/share.component.ts b/libs/angular/src/components/share.component.ts index 7f11210f9a..480e0cc291 100644 --- a/libs/angular/src/components/share.component.ts +++ b/libs/angular/src/components/share.component.ts @@ -6,6 +6,7 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; import { LogService } from "@bitwarden/common/platform/abstractions/log.service"; import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service"; @@ -26,6 +27,7 @@ export class ShareComponent implements OnInit, OnDestroy { organizations$: Observable; protected writeableCollections: Checkable[] = []; + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); private _destroy = new Subject(); @@ -49,7 +51,9 @@ export class ShareComponent implements OnInit, OnDestroy { } async load() { - const allCollections = await this.collectionService.getAllDecrypted(); + const allCollections = await firstValueFrom( + this.collectionService.decryptedCollections$(this.activeUserId$), + ); this.writeableCollections = allCollections.map((c) => c).filter((c) => !c.readOnly); this.organizations$ = this.organizationService.memberOrganizations$.pipe( diff --git a/libs/angular/src/vault/components/add-edit.component.ts b/libs/angular/src/vault/components/add-edit.component.ts index 44eaec03a6..1c612e7b05 100644 --- a/libs/angular/src/vault/components/add-edit.component.ts +++ b/libs/angular/src/vault/components/add-edit.component.ts @@ -39,6 +39,7 @@ import { SecureNoteView } from "@bitwarden/common/vault/models/view/secure-note. import { CipherAuthorizationService } from "@bitwarden/common/vault/services/cipher-authorization.service"; import { DialogService } from "@bitwarden/components"; import { PasswordRepromptService } from "@bitwarden/vault"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; @Directive() export class AddEditComponent implements OnInit, OnDestroy { @@ -98,6 +99,8 @@ export class AddEditComponent implements OnInit, OnDestroy { private personalOwnershipPolicyAppliesToActiveUser: boolean; private previousCipherId: string; + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + get fido2CredentialCreationDateValue(): string { const dateCreated = this.i18nService.t("dateCreated"); const creationDate = this.datePipe.transform( @@ -253,9 +256,7 @@ export class AddEditComponent implements OnInit, OnDestroy { if (this.cipher == null) { if (this.editMode) { const cipher = await this.loadCipher(); - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); this.cipher = await cipher.decrypt( await this.cipherService.getKeyForCipherKeyDecryption(cipher, activeUserId), ); @@ -391,9 +392,7 @@ export class AddEditComponent implements OnInit, OnDestroy { this.cipher.id = null; } - const activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); + const activeUserId = await firstValueFrom(this.activeUserId$); const cipher = await this.encryptCipher(activeUserId); try { this.formPromise = this.saveCipher(cipher); @@ -672,7 +671,9 @@ export class AddEditComponent implements OnInit, OnDestroy { } protected async loadCollections() { - const allCollections = await this.collectionService.getAllDecrypted(); + const allCollections = await firstValueFrom( + this.collectionService.decryptedCollections$(this.activeUserId$), + ); return allCollections.filter((c) => !c.readOnly); } diff --git a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts index 34310610ca..98e861e5f5 100644 --- a/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts +++ b/libs/angular/src/vault/vault-filter/services/vault-filter.service.ts @@ -9,6 +9,8 @@ import { import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { ActiveUserState, StateProvider } from "@bitwarden/common/platform/state"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -30,6 +32,8 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti private readonly collapsedGroupings$: Observable> = this.collapsedGroupingsState.state$.pipe(map((c) => new Set(c))); + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( protected organizationService: OrganizationService, protected folderService: FolderService, @@ -37,6 +41,7 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti protected collectionService: CollectionService, protected policyService: PolicyService, protected stateProvider: StateProvider, + protected accountService: AccountService, ) {} async storeCollapsedFilterNodes(collapsedFilterNodes: Set): Promise { @@ -85,14 +90,16 @@ export class VaultFilterService implements DeprecatedVaultFilterServiceAbstracti } async buildCollections(organizationId?: string): Promise> { - const storedCollections = await this.collectionService.getAllDecrypted(); + const storedCollections = await firstValueFrom( + this.collectionService.decryptedCollections$(this.activeUserId$), + ); let collections: CollectionView[]; if (organizationId != null) { collections = storedCollections.filter((c) => c.organizationId === organizationId); } else { collections = storedCollections; } - const nestedCollections = await this.collectionService.getAllNested(collections); + const nestedCollections = this.collectionService.getAllNested(collections); return new DynamicTreeNode({ fullList: collections, nestedList: nestedCollections, diff --git a/libs/common/src/auth/services/account.service.ts b/libs/common/src/auth/services/account.service.ts index dceec2cbf1..04a0c62dd9 100644 --- a/libs/common/src/auth/services/account.service.ts +++ b/libs/common/src/auth/services/account.service.ts @@ -45,6 +45,24 @@ const LOGGED_OUT_INFO: AccountInfo = { name: undefined, }; +/** + * An rxjs map operator that extracts the UserId from an account, or throws if the account or UserId are null. + */ +export const getUserId = map<{ id: UserId | undefined }, UserId>((account) => { + if (account?.id == null) { + throw new Error("Null account or account ID"); + } + + return account.id; +}); + +/** + * An rxjs map operator that extracts the UserId from an account, or returns undefined if the account or UserId are null. + */ +export const getOptionalUserId = map<{ id: UserId | undefined }, UserId | undefined>( + (account) => account?.id ?? undefined, +); + export class AccountServiceImplementation implements InternalAccountService { private accountsState: GlobalState>; private activeAccountIdState: GlobalState; diff --git a/libs/common/src/platform/sync/core-sync.service.ts b/libs/common/src/platform/sync/core-sync.service.ts index 13f1525afd..7703556e39 100644 --- a/libs/common/src/platform/sync/core-sync.service.ts +++ b/libs/common/src/platform/sync/core-sync.service.ts @@ -6,6 +6,7 @@ import { ApiService } from "../../abstractions/api.service"; import { AccountService } from "../../auth/abstractions/account.service"; import { AuthService } from "../../auth/abstractions/auth.service"; import { AuthenticationStatus } from "../../auth/enums/authentication-status"; +import { getUserId } from "../../auth/services/account.service"; import { SyncCipherNotification, SyncFolderNotification, @@ -149,7 +150,10 @@ export abstract class CoreSyncService implements SyncService { notification.collectionIds != null && notification.collectionIds.length > 0 ) { - const collections = await this.collectionService.getAll(); + const activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + const collections = await firstValueFrom( + this.collectionService.encryptedCollections$(activeUserId$), + ); if (collections != null) { for (let i = 0; i < collections.length; i++) { if (notification.collectionIds.indexOf(collections[i].id) > -1) { diff --git a/libs/common/src/services/vault-timeout/vault-timeout.service.ts b/libs/common/src/services/vault-timeout/vault-timeout.service.ts index 07c3cd56f1..4ff46d475d 100644 --- a/libs/common/src/services/vault-timeout/vault-timeout.service.ts +++ b/libs/common/src/services/vault-timeout/vault-timeout.service.ts @@ -134,9 +134,13 @@ export class VaultTimeoutService implements VaultTimeoutServiceAbstraction { if (userId == null || userId === currentUserId) { await this.searchService.clearIndex(); await this.folderService.clearCache(); - await this.collectionService.clearActiveUserCache(); } + // TODO: is the userId ever null here? Surely a null userId cannot be authenticated? + // Also MasterPasswordService throws an exception if the userId is null and hasn't caused any issues + // TODO: why do these other services clear lockingUserId and not userId? + await this.collectionService.clearDecryptedState(userId); + await this.masterPasswordService.clearMasterKey(lockingUserId); await this.stateService.setUserKeyAutoUnlock(null, { userId: lockingUserId }); diff --git a/libs/importer/src/components/import.component.ts b/libs/importer/src/components/import.component.ts index 1ffe2728b0..74c61a89da 100644 --- a/libs/importer/src/components/import.component.ts +++ b/libs/importer/src/components/import.component.ts @@ -72,6 +72,7 @@ import { ImportSuccessDialogComponent, } from "./dialog"; import { ImportLastPassComponent } from "./lastpass"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; const safeProviders: SafeProvider[] = [ safeProvider({ @@ -129,6 +130,8 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { collections$: Observable; organizations$: Observable; + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + private _organizationId: string; get organizationId(): string { @@ -205,6 +208,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { @Optional() protected importCollectionService: ImportCollectionServiceAbstraction, protected toastService: ToastService, + protected accountService: AccountService, ) {} protected get importBlockedByPolicy(): boolean { @@ -272,15 +276,15 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit { } if (value) { - this.collections$ = Utils.asyncToObservable(() => - this.collectionService - .getAllDecrypted() - .then((decryptedCollections) => + this.collections$ = this.collectionService + .decryptedCollections$(this.activeUserId$) + .pipe( + map((decryptedCollections) => decryptedCollections .filter((c2) => c2.organizationId === value && c2.manage) .sort(Utils.getSortFunction(this.i18nService, "name")), ), - ); + ); } }); this.formGroup.controls.vaultSelector.setValue("myVault"); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index 4e23a0ed25..6ed1be224c 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -33,11 +33,14 @@ import { import { BaseVaultExportService } from "./base-vault-export.service"; import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; import { ExportFormat } from "./vault-export.service.abstraction"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; export class OrganizationVaultExportService extends BaseVaultExportService implements OrganizationVaultExportServiceAbstraction { + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( private cipherService: CipherService, private apiService: ApiService, @@ -185,9 +188,13 @@ export class OrganizationVaultExportService const promises = []; promises.push( - this.collectionService.getAllDecrypted().then(async (collections) => { - decCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); - }), + firstValueFrom(this.collectionService.decryptedCollections$(this.activeUserId$)).then( + (collections) => { + decCollections = collections.filter( + (c) => c.organizationId == organizationId && c.manage, + ); + }, + ), ); promises.push( @@ -217,9 +224,13 @@ export class OrganizationVaultExportService const promises = []; promises.push( - this.collectionService.getAll().then((collections) => { - encCollections = collections.filter((c) => c.organizationId == organizationId && c.manage); - }), + firstValueFrom(this.collectionService.encryptedCollections$(this.activeUserId$)).then( + (collections) => { + encCollections = collections.filter( + (c) => c.organizationId == organizationId && c.manage, + ); + }, + ), ); promises.push( diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 0c36280442..cc78890dff 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -21,6 +21,8 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { PolicyType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { EventType } from "@bitwarden/common/enums"; import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; @@ -150,6 +152,8 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { { name: ".json (Encrypted)", value: "encrypted_json" }, ]; + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + private destroy$ = new Subject(); private onlyManagedCollections = true; @@ -167,6 +171,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { protected dialogService: DialogService, protected organizationService: OrganizationService, private collectionService: CollectionService, + protected accountService: AccountService, ) {} async ngOnInit() { @@ -204,7 +209,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } this.organizations$ = combineLatest({ - collections: this.collectionService.decryptedCollections$, + collections: this.collectionService.decryptedCollections$(this.activeUserId$), memberOrganizations: this.organizationService.memberOrganizations$, }).pipe( map(({ collections, memberOrganizations }) => { diff --git a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts index 6b607e3048..fb71257ea2 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form-config.service.ts @@ -5,6 +5,8 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction"; import { OrganizationUserStatusType, PolicyType } from "@bitwarden/common/admin-console/enums"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CipherId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; @@ -29,6 +31,9 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { private cipherService: CipherService = inject(CipherService); private folderService: FolderService = inject(FolderService); private collectionService: CollectionService = inject(CollectionService); + private accountService = inject(AccountService); + + protected activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); async buildConfig( mode: CipherFormMode, @@ -39,9 +44,9 @@ export class DefaultCipherFormConfigService implements CipherFormConfigService { await firstValueFrom( combineLatest([ this.organizations$, - this.collectionService.encryptedCollections$.pipe( + this.collectionService.encryptedCollections$(this.activeUserId$).pipe( switchMap((c) => - this.collectionService.decryptedCollections$.pipe( + this.collectionService.decryptedCollections$(this.activeUserId$).pipe( filter((d) => d.length === c.length), // Ensure all collections have been decrypted ), ), diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 324b2358a8..08e8571000 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -1,13 +1,14 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; -import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; -import { CollectionId } from "@bitwarden/common/types/guid"; import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; @@ -56,10 +57,13 @@ export class CipherViewComponent implements OnChanges, OnDestroy { private destroyed$: Subject = new Subject(); cardIsExpired: boolean = false; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( private organizationService: OrganizationService, private collectionService: CollectionService, private folderService: FolderService, + private accountService: AccountService, ) {} async ngOnChanges() { @@ -98,9 +102,13 @@ export class CipherViewComponent implements OnChanges, OnDestroy { (!this.collections || this.collections.length === 0) ) { this.collections = await firstValueFrom( - this.collectionService.decryptedCollectionViews$( - this.cipher.collectionIds as CollectionId[], - ), + this.collectionService + .decryptedCollections$(this.activeUserId$) + .pipe( + map((allCollections) => + allCollections.filter((c) => this.cipher.collectionIds.includes(c.id)), + ), + ), ); } diff --git a/libs/vault/src/components/assign-collections.component.ts b/libs/vault/src/components/assign-collections.component.ts index fed1dde28e..25b76f90c6 100644 --- a/libs/vault/src/components/assign-collections.component.ts +++ b/libs/vault/src/components/assign-collections.component.ts @@ -28,8 +28,9 @@ import { OrganizationService } from "@bitwarden/common/admin-console/abstraction import { OrganizationUserStatusType } from "@bitwarden/common/admin-console/enums"; import { Organization } from "@bitwarden/common/admin-console/models/domain/organization"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service"; -import { CipherId, CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CipherId, CollectionId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { @@ -170,7 +171,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI private get selectedOrgId(): OrganizationId { return this.formGroup.getRawValue().selectedOrg || this.params.organizationId; } - private activeUserId: UserId; + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); private destroy$ = new Subject(); constructor( @@ -184,10 +185,6 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI ) {} async ngOnInit() { - this.activeUserId = await firstValueFrom( - this.accountService.activeAccount$.pipe(map((a) => a?.id)), - ); - const onlyPersonalItems = this.params.ciphers.every((c) => c.organizationId == null); if (this.selectedOrgId === MY_VAULT_ID || onlyPersonalItems) { @@ -405,7 +402,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI */ private getCollectionsForOrganization(orgId: OrganizationId): Observable { return combineLatest([ - this.collectionService.decryptedCollections$, + this.collectionService.decryptedCollections$(this.activeUserId$), this.organizationService.organizations$, ]).pipe( map(([collections, organizations]) => { @@ -429,7 +426,7 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI shareableCiphers, organizationId, selectedCollectionIds, - this.activeUserId, + await firstValueFrom(this.activeUserId$), ); this.toastService.showToast({ @@ -470,7 +467,10 @@ export class AssignCollectionsComponent implements OnInit, OnDestroy, AfterViewI private async updateAssignedCollections(cipherView: CipherView) { const { collections } = this.formGroup.getRawValue(); cipherView.collectionIds = collections.map((i) => i.id as CollectionId); - const cipher = await this.cipherService.encrypt(cipherView, this.activeUserId); + const cipher = await this.cipherService.encrypt( + cipherView, + await firstValueFrom(this.activeUserId$), + ); if (this.params.isSingleCipherAdmin) { await this.cipherService.saveCollectionsWithServerAdmin(cipher); } else {