diff --git a/libs/common/src/vault/abstractions/cipher-sdk.service.ts b/libs/common/src/vault/abstractions/cipher-sdk.service.ts index 805a30221ca..dc9fd943990 100644 --- a/libs/common/src/vault/abstractions/cipher-sdk.service.ts +++ b/libs/common/src/vault/abstractions/cipher-sdk.service.ts @@ -1,5 +1,7 @@ import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; +import { CipherListView } from "@bitwarden/sdk-internal"; /** * Result of decrypting all ciphers, containing both successes and failures. @@ -126,16 +128,17 @@ export abstract class CipherSdkService { abstract getAllDecrypted(userId: UserId): Promise; /** - * Fetches and decrypts all ciphers for an organization from the API using the SDK. + * Fetches all ciphers for an organization from the API using the SDK. + * Returns encrypted ciphers for on-demand decryption and lightweight list views for display. * * @param organizationId The organization ID to fetch ciphers for * @param userId The user ID to use for SDK client * @param includeMemberItems Whether to include member items - * @returns A promise that resolves to the decrypted cipher views + * @returns A promise that resolves to the encrypted ciphers and decrypted list views */ abstract getAllFromApiForOrganization( organizationId: string, userId: UserId, includeMemberItems: boolean, - ): Promise; + ): Promise<[Cipher[], CipherListView[]]>; } diff --git a/libs/common/src/vault/services/cipher-sdk.service.spec.ts b/libs/common/src/vault/services/cipher-sdk.service.spec.ts index 7640627b774..dd8c0566018 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.spec.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.spec.ts @@ -7,6 +7,7 @@ import { UserId, CipherId, OrganizationId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherType } from "../enums/cipher-type"; +import { Cipher } from "../models/domain/cipher"; import { DefaultCipherSdkService } from "./cipher-sdk.service"; @@ -34,7 +35,7 @@ describe("DefaultCipherSdkService", () => { soft_delete_many: jest.fn().mockResolvedValue(undefined), restore: jest.fn().mockResolvedValue(undefined), restore_many: jest.fn().mockResolvedValue(undefined), - list_org_ciphers: jest.fn().mockResolvedValue({ successes: [], failures: [] }), + list_org_ciphers: jest.fn().mockResolvedValue({ ciphers: [], listViews: [] }), }; mockCiphersSdk = { create: jest.fn(), @@ -613,12 +614,40 @@ describe("DefaultCipherSdkService", () => { }); describe("getAllFromApiForOrganization()", () => { + const mockSdkCipher: any = { + id: cipherId, + name: "2.encryptedName|iv|data", + type: CipherType.Login, + organizationId: orgId, + folderId: null, + favorite: false, + edit: true, + viewPassword: true, + organizationUseTotp: false, + revisionDate: new Date().toISOString(), + creationDate: new Date().toISOString(), + collectionIds: [], + deletedDate: null, + reprompt: 0, + key: null, + localData: null, + attachments: null, + fields: null, + passwordHistory: null, + notes: null, + login: null, + secureNote: null, + card: null, + identity: null, + sshKey: null, + permissions: null, + }; + it("should list organization ciphers using SDK admin API", async () => { - const mockSdkCipherView = new CipherView().toSdkCipherView(); - mockSdkCipherView.name = "Org Cipher"; + const mockListView: any = { id: cipherId, name: "Org Cipher" }; mockAdminSdk.list_org_ciphers.mockResolvedValue({ - successes: [mockSdkCipherView], - failures: [], + ciphers: [mockSdkCipher], + listViews: [mockListView], }); const result = await cipherSdkService.getAllFromApiForOrganization(orgId, userId, false); @@ -627,14 +656,16 @@ describe("DefaultCipherSdkService", () => { expect(mockVaultSdk.ciphers).toHaveBeenCalled(); expect(mockCiphersSdk.admin).toHaveBeenCalled(); expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(orgId, false); - expect(result).toHaveLength(1); - expect(result[0]).toBeInstanceOf(CipherView); + const [ciphers, listViews] = result; + expect(ciphers).toHaveLength(1); + expect(ciphers[0]).toBeInstanceOf(Cipher); + expect(listViews).toHaveLength(1); }); it("should pass includeMemberItems parameter to SDK", async () => { mockAdminSdk.list_org_ciphers.mockResolvedValue({ - successes: [], - failures: [], + ciphers: [], + listViews: [], }); await cipherSdkService.getAllFromApiForOrganization(orgId, userId, true); diff --git a/libs/common/src/vault/services/cipher-sdk.service.ts b/libs/common/src/vault/services/cipher-sdk.service.ts index 4e7c877f8c9..2f2c2049434 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.ts @@ -5,7 +5,7 @@ import { LogService } from "@bitwarden/common/platform/abstractions/log.service" import { SdkService, asUuid } from "@bitwarden/common/platform/abstractions/sdk/sdk.service"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; +import { CipherListView, CipherView as SdkCipherView } from "@bitwarden/sdk-internal"; import { CipherSdkService, DecryptAllCiphersResult } from "../abstractions/cipher-sdk.service"; import { Cipher } from "../models/domain/cipher"; @@ -299,7 +299,7 @@ export class DefaultCipherSdkService implements CipherSdkService { organizationId: string, userId: UserId, includeMemberItems: boolean, - ): Promise { + ): Promise<[Cipher[], CipherListView[]]> { return await firstValueFrom( this.sdkService.userClient$(userId).pipe( switchMap(async (sdk) => { @@ -308,15 +308,17 @@ export class DefaultCipherSdkService implements CipherSdkService { } using ref = sdk.take(); - const decryptResult = await ref.value + const result = await ref.value .vault() .ciphers() .admin() .list_org_ciphers(asUuid(organizationId), includeMemberItems); - return decryptResult.successes - .map((sdkCipherView: any) => CipherView.fromSdkCipherView(sdkCipherView)) - .filter((v): v is CipherView => v !== undefined); + const ciphers = result.ciphers + .map((c) => Cipher.fromSdkCipher(c)) + .filter((c): c is Cipher => c !== undefined); + + return [ciphers, result.listViews] as [Cipher[], CipherListView[]]; }), catchError((error: unknown) => { this.logService.error(`Failed to list organization ciphers: ${error}`); diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index e77faf26edc..fca13863401 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1280,6 +1280,9 @@ describe("Cipher Service", () => { it("should use SDK to list organization ciphers when feature flag is enabled", async () => { sdkCrudFeatureFlag$.next(true); + const mockCipher1 = new Cipher(cipherData); + const mockCipher2 = new Cipher(cipherData); + const mockCipherView1 = new CipherView(); mockCipherView1.name = "Test Cipher 1"; const mockCipherView2 = new CipherView(); @@ -1287,7 +1290,12 @@ describe("Cipher Service", () => { const sdkServiceSpy = jest .spyOn(cipherSdkService, "getAllFromApiForOrganization") - .mockResolvedValue([mockCipherView1, mockCipherView2]); + .mockResolvedValue([[mockCipher1, mockCipher2], []]); + + cipherEncryptionService.decryptManyLegacy.mockResolvedValue([ + [mockCipherView1, mockCipherView2], + [], + ]); const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); @@ -1295,6 +1303,10 @@ describe("Cipher Service", () => { expect(sdkServiceSpy).toHaveBeenCalledWith(testOrgId, mockUserId, true); expect(apiSpy).not.toHaveBeenCalled(); + expect(cipherEncryptionService.decryptManyLegacy).toHaveBeenCalledWith( + [mockCipher1, mockCipher2], + mockUserId, + ); expect(result).toHaveLength(2); expect(result[0]).toBeInstanceOf(CipherView); expect(result[1]).toBeInstanceOf(CipherView); @@ -1305,7 +1317,9 @@ describe("Cipher Service", () => { const sdkServiceSpy = jest .spyOn(cipherSdkService, "getAllFromApiForOrganization") - .mockResolvedValue([]); + .mockResolvedValue([[], []]); + + cipherEncryptionService.decryptManyLegacy.mockResolvedValue([[], []]); const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); @@ -1323,9 +1337,7 @@ describe("Cipher Service", () => { }); it("should use SDK to list and decrypt ciphers when feature flag is enabled", async () => { - configService.getFeatureFlag - .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) - .mockResolvedValue(true); + sdkCrudFeatureFlag$.next(true); const mockCipherView1 = new CipherView(); mockCipherView1.name = "Test Cipher 1"; diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index 678f9d4a385..7972dd786f3 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -788,12 +788,14 @@ export class CipherService implements CipherServiceAbstraction { } try { - const cipherViews = await this.cipherSdkService.getAllFromApiForOrganization( + const [ciphers] = await this.cipherSdkService.getAllFromApiForOrganization( organizationId, userId, includeMemberItems, ); + const [cipherViews] = await this.cipherEncryptionService.decryptManyLegacy(ciphers, userId); + // Sort by locale (matching existing behavior) cipherViews.sort(this.getLocaleSortingFunction()); @@ -975,7 +977,7 @@ export class CipherService implements CipherServiceAbstraction { } const encrypted = await this.encrypt(cipherView, userId); - const result = await this.createWithServer_legacy(encrypted, orgAdmin); + const result = await this.createWithServerLegacy(encrypted, orgAdmin); return await this.decrypt(result, userId); } @@ -993,7 +995,7 @@ export class CipherService implements CipherServiceAbstraction { return resultCipherView; } - private async createWithServer_legacy( + private async createWithServerLegacy( { cipher, encryptedFor }: EncryptionContext, orgAdmin?: boolean, ): Promise { @@ -1032,7 +1034,7 @@ export class CipherService implements CipherServiceAbstraction { } const encrypted = await this.encrypt(cipherView, userId); - const updatedCipher = await this.updateWithServer_legacy(encrypted, orgAdmin); + const updatedCipher = await this.updateWithServerLegacy(encrypted, orgAdmin); const updatedCipherView = await this.decrypt(updatedCipher, userId); return updatedCipherView; } @@ -1053,7 +1055,7 @@ export class CipherService implements CipherServiceAbstraction { return resultCipherView; } - async updateWithServer_legacy( + async updateWithServerLegacy( { cipher, encryptedFor }: EncryptionContext, orgAdmin?: boolean, ): Promise {