diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index ef021143ce8..4194a569b6b 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -1490,6 +1490,116 @@ describe("Cipher Service", () => { }); }); + describe("getAllFromApiForOrganization()", () => { + let mockSdkClient: any; + let mockCiphersSdk: any; + let mockAdminSdk: any; + let mockVaultSdk: any; + const testOrgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b21" as OrganizationId; + const mockSdkCipherView1 = { + id: "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22", + name: "Test Cipher 1", + }; + const mockSdkCipherView2 = { + id: "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23", + name: "Test Cipher 2", + }; + + beforeEach(() => { + // Mock the SDK client chain for list_org_ciphers + mockAdminSdk = { + list_org_ciphers: jest.fn().mockResolvedValue({ + successes: [mockSdkCipherView1, mockSdkCipherView2], + failures: [], + }), + }; + mockCiphersSdk = { + admin: jest.fn().mockReturnValue(mockAdminSdk), + }; + mockVaultSdk = { + ciphers: jest.fn().mockReturnValue(mockCiphersSdk), + }; + const mockSdkValue = { + vault: jest.fn().mockReturnValue(mockVaultSdk), + }; + mockSdkClient = { + take: jest.fn().mockReturnValue({ + value: mockSdkValue, + [Symbol.dispose]: jest.fn(), + }), + }; + + // Mock sdkService to return the mock client + sdkService.userClient$.mockReturnValue(of(mockSdkClient)); + }); + + it("should call apiService.getCiphersOrganization when feature flag is disabled", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) + .mockResolvedValue(false); + + const mockResponse = { + data: [], + } as any; + + const apiSpy = jest + .spyOn(apiService, "getCiphersOrganization") + .mockResolvedValue(mockResponse); + + await cipherService.getAllFromApiForOrganization(testOrgId, true); + + expect(apiSpy).toHaveBeenCalledWith(testOrgId, true); + expect(mockSdkClient.take).not.toHaveBeenCalled(); + }); + + it("should call apiService.getCiphersOrganization without includeMemberItems when not provided", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) + .mockResolvedValue(false); + + const mockResponse = { data: [] } as any; + const apiSpy = jest + .spyOn(apiService, "getCiphersOrganization") + .mockResolvedValue(mockResponse); + + await cipherService.getAllFromApiForOrganization(testOrgId); + + expect(apiSpy).toHaveBeenCalledWith(testOrgId, undefined); + expect(mockSdkClient.take).not.toHaveBeenCalled(); + }); + + it("should use SDK to list organization ciphers when feature flag is enabled", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) + .mockResolvedValue(true); + + const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); + + const result = await cipherService.getAllFromApiForOrganization(testOrgId, true); + + expect(mockSdkClient.take).toHaveBeenCalled(); + expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(testOrgId, true); + expect(apiSpy).not.toHaveBeenCalled(); + expect(result).toHaveLength(2); + expect(result[0]).toBeInstanceOf(CipherView); + expect(result[1]).toBeInstanceOf(CipherView); + }); + + it("should use SDK with includeMemberItems=false when not provided", async () => { + configService.getFeatureFlag + .calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations) + .mockResolvedValue(true); + + const apiSpy = jest.spyOn(apiService, "getCiphersOrganization"); + + await cipherService.getAllFromApiForOrganization(testOrgId); + + expect(mockSdkClient.take).toHaveBeenCalled(); + expect(mockAdminSdk.list_org_ciphers).toHaveBeenCalledWith(testOrgId, false); + expect(apiSpy).not.toHaveBeenCalled(); + }); + }); + describe("replace (no upsert)", () => { // In order to set up initial state we need to manually update the encrypted state // which will result in an emission. All tests will have this baseline emission. diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index e28468ecb37..a61890ce810 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -723,6 +723,13 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, includeMemberItems?: boolean, ): Promise { + const useSdk = await this.configService.getFeatureFlag( + FeatureFlag.PM27632_SdkCipherCrudOperations, + ); + if (useSdk) { + return this.getAllFromApiForOrganization_sdk(organizationId, includeMemberItems ?? false); + } + const response = await this.apiService.getCiphersOrganization( organizationId, includeMemberItems, @@ -730,6 +737,48 @@ export class CipherService implements CipherServiceAbstraction { return await this.decryptOrganizationCiphersResponse(response, organizationId); } + private async getAllFromApiForOrganization_sdk( + organizationId: string, + includeMemberItems: boolean, + ): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(map((a) => a?.id))); + if (!userId) { + throw new Error("User ID is required"); + } + + const result = await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + switchMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + const decryptResult = await ref.value + .vault() + .ciphers() + .admin() + .list_org_ciphers(asUuid(organizationId), includeMemberItems); + + // Convert successful decryptions to CipherView[] + const cipherViews = decryptResult.successes.map((sdkCipherView: any) => + CipherView.fromSdkCipherView(sdkCipherView), + ); + + // Sort by locale (matching existing behavior) + cipherViews.sort(this.getLocaleSortingFunction()); + + return cipherViews; + }), + catchError((error: unknown) => { + this.logService.error(`Failed to list organization ciphers: ${error}`); + return []; + }), + ), + ); + + return result; + } + async getManyFromApiForOrganization(organizationId: string): Promise { const r = await this.apiService.send( "GET",