diff --git a/libs/common/src/platform/misc/index.ts b/libs/common/src/platform/misc/index.ts new file mode 100644 index 0000000000..56fc18c282 --- /dev/null +++ b/libs/common/src/platform/misc/index.ts @@ -0,0 +1 @@ +export * from "./rxjs-operators"; diff --git a/libs/common/src/platform/misc/rxjs-operators.spec.ts b/libs/common/src/platform/misc/rxjs-operators.spec.ts new file mode 100644 index 0000000000..e31c774545 --- /dev/null +++ b/libs/common/src/platform/misc/rxjs-operators.spec.ts @@ -0,0 +1,58 @@ +import { firstValueFrom, of } from "rxjs"; + +import { getById, getByIds } from "./rxjs-operators"; + +describe("custom rxjs operators", () => { + describe("getById", () => { + it("returns an object with a matching id", async () => { + const input = [ + { + id: 1, + data: "one", + }, + { + id: 2, + data: "two", + }, + { + id: 3, + data: "three", + }, + ]; + + const output = await firstValueFrom(getById(2)(of(input))); + + expect(output).toEqual([{ id: 1, data: "one" }]); + }); + }); + + describe("getByIds", () => { + it("returns an array of objects with matching ids", async () => { + const input = [ + { + id: 1, + data: "one", + }, + { + id: 2, + data: "two", + }, + { + id: 3, + data: "three", + }, + { + id: 4, + data: "four", + }, + ]; + + const output = await firstValueFrom(getByIds([2, 3])(of(input))); + + expect(output).toEqual([ + { id: 2, data: "two" }, + { id: 3, data: "three" }, + ]); + }); + }); +}); diff --git a/libs/common/src/platform/misc/rxjs-operators.ts b/libs/common/src/platform/misc/rxjs-operators.ts new file mode 100644 index 0000000000..b8e743cee3 --- /dev/null +++ b/libs/common/src/platform/misc/rxjs-operators.ts @@ -0,0 +1,9 @@ +import { map } from "rxjs"; + +type ObjectWithId = { id: TId }; + +export const getById = >(id: TId) => + map((objects) => objects.find((o) => o.id === id)); + +export const getByIds = >(ids: TId[]) => + map((objects) => objects.filter((o) => ids.includes(o.id))); diff --git a/libs/common/src/vault/services/cipher-authorization.service.spec.ts b/libs/common/src/vault/services/cipher-authorization.service.spec.ts index cccd29ad69..d43dff6520 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.spec.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.spec.ts @@ -4,8 +4,9 @@ import { firstValueFrom, of } 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 { CollectionId } from "@bitwarden/common/types/guid"; +import { CollectionId, UserId } from "@bitwarden/common/types/guid"; +import { mockAccountServiceWith } from "../../../spec"; import { CipherView } from "../models/view/cipher.view"; import { @@ -14,10 +15,13 @@ import { } from "./cipher-authorization.service"; describe("CipherAuthorizationService", () => { + const userId = "UserId" as UserId; + let cipherAuthorizationService: CipherAuthorizationService; const mockCollectionService = mock(); const mockOrganizationService = mock(); + const mockAccountService = mockAccountServiceWith(userId); // Mock factories const createMockCipher = ( @@ -56,6 +60,7 @@ describe("CipherAuthorizationService", () => { cipherAuthorizationService = new DefaultCipherAuthorizationService( mockCollectionService, mockOrganizationService, + mockAccountService, ); }); @@ -113,7 +118,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", true), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); @@ -121,10 +126,6 @@ describe("CipherAuthorizationService", () => { .canDeleteCipher$(cipher, [activeCollectionId]) .subscribe((result) => { expect(result).toBe(true); - expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ - "col1", - "col2", - ] as CollectionId[]); done(); }); }); @@ -139,7 +140,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", false), createMockCollection("col2", true), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); @@ -147,10 +148,6 @@ describe("CipherAuthorizationService", () => { .canDeleteCipher$(cipher, [activeCollectionId]) .subscribe((result) => { expect(result).toBe(false); - expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ - "col1", - "col2", - ] as CollectionId[]); done(); }); }); @@ -165,17 +162,12 @@ describe("CipherAuthorizationService", () => { createMockCollection("col2", true), createMockCollection("col3", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { expect(result).toBe(true); - expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ - "col1", - "col2", - "col3", - ] as CollectionId[]); done(); }); }); @@ -189,16 +181,12 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", false), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); cipherAuthorizationService.canDeleteCipher$(cipher).subscribe((result) => { expect(result).toBe(false); - expect(mockCollectionService.decryptedCollectionViews$).toHaveBeenCalledWith([ - "col1", - "col2", - ] as CollectionId[]); done(); }); }); @@ -246,7 +234,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", true), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); @@ -263,7 +251,7 @@ describe("CipherAuthorizationService", () => { createMockCollection("col1", false), createMockCollection("col2", false), ]; - mockCollectionService.decryptedCollectionViews$.mockReturnValue( + mockCollectionService.decryptedCollections$.mockReturnValue( of(allCollections as CollectionView[]), ); diff --git a/libs/common/src/vault/services/cipher-authorization.service.ts b/libs/common/src/vault/services/cipher-authorization.service.ts index eb6211848a..2772ffbe6f 100644 --- a/libs/common/src/vault/services/cipher-authorization.service.ts +++ b/libs/common/src/vault/services/cipher-authorization.service.ts @@ -4,6 +4,9 @@ import { CollectionService } from "@bitwarden/admin-console/common"; import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction"; import { CollectionId } from "@bitwarden/common/types/guid"; +import { AccountService } from "../../auth/abstractions/account.service"; +import { getUserId } from "../../auth/services/account.service"; +import { getByIds } from "../../platform/misc/rxjs-operators"; import { Cipher } from "../models/domain/cipher"; import { CipherView } from "../models/view/cipher.view"; @@ -46,9 +49,12 @@ export abstract class CipherAuthorizationService { * {@link CipherAuthorizationService} */ export class DefaultCipherAuthorizationService implements CipherAuthorizationService { + private activeUserId$ = this.accountService.activeAccount$.pipe(getUserId); + constructor( private collectionService: CollectionService, private organizationService: OrganizationService, + private accountService: AccountService, ) {} /** @@ -77,19 +83,18 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer } } - return this.collectionService - .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) - .pipe( - map((allCollections) => { - const shouldFilter = allowedCollections?.some(Boolean); + return this.collectionService.decryptedCollections$(this.activeUserId$).pipe( + getByIds(cipher.collectionIds), + map((cipherCollections) => { + const shouldFilter = allowedCollections?.some(Boolean); - const collections = shouldFilter - ? allCollections.filter((c) => allowedCollections.includes(c.id as CollectionId)) - : allCollections; + const collections = shouldFilter + ? cipherCollections.filter((c) => allowedCollections.includes(c.id as CollectionId)) + : cipherCollections; - return collections.some((collection) => collection.manage); - }), - ); + return collections.some((collection) => collection.manage); + }), + ); }), ); } @@ -112,9 +117,10 @@ export class DefaultCipherAuthorizationService implements CipherAuthorizationSer return of(true); } - return this.collectionService - .decryptedCollectionViews$(cipher.collectionIds as CollectionId[]) - .pipe(map((allCollections) => allCollections.some((collection) => collection.manage))); + return this.collectionService.decryptedCollections$(this.activeUserId$).pipe( + getByIds(cipher.collectionIds), + map((allCollections) => allCollections.some((collection) => collection.manage)), + ); }), shareReplay({ bufferSize: 1, refCount: false }), ); diff --git a/libs/vault/src/cipher-view/cipher-view.component.ts b/libs/vault/src/cipher-view/cipher-view.component.ts index 08e8571000..50c0b6cadf 100644 --- a/libs/vault/src/cipher-view/cipher-view.component.ts +++ b/libs/vault/src/cipher-view/cipher-view.component.ts @@ -1,6 +1,6 @@ import { CommonModule } from "@angular/common"; import { Component, Input, OnChanges, OnDestroy } from "@angular/core"; -import { firstValueFrom, map, Observable, Subject, takeUntil } from "rxjs"; +import { firstValueFrom, Observable, Subject, takeUntil } from "rxjs"; import { CollectionService, CollectionView } from "@bitwarden/admin-console/common"; import { JslibModule } from "@bitwarden/angular/jslib.module"; @@ -9,6 +9,7 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { isCardExpired } from "@bitwarden/common/autofill/utils"; +import { getByIds } from "@bitwarden/common/platform/misc"; 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"; @@ -104,11 +105,7 @@ export class CipherViewComponent implements OnChanges, OnDestroy { this.collections = await firstValueFrom( this.collectionService .decryptedCollections$(this.activeUserId$) - .pipe( - map((allCollections) => - allCollections.filter((c) => this.cipher.collectionIds.includes(c.id)), - ), - ), + .pipe(getByIds(this.cipher.collectionIds)), ); }