1
0
mirror of https://github.com/bitwarden/browser synced 2026-02-18 18:33:50 +00:00

Implement getAllDecrypted and getAllFromApiForOrganization for SDK

This commit is contained in:
Nik Gilmore
2026-01-09 15:30:23 -08:00
parent ddd9a16acd
commit 0eed00be6f
2 changed files with 124 additions and 3 deletions

View File

@@ -46,7 +46,7 @@ import { CipherView } from "../models/view/cipher.view";
import { LoginUriView } from "../models/view/login-uri.view";
import { CipherService } from "./cipher.service";
import { ENCRYPTED_CIPHERS } from "./key-state/ciphers.state";
import { DECRYPTED_CIPHERS, ENCRYPTED_CIPHERS } from "./key-state/ciphers.state";
const ENCRYPTED_TEXT = "This data has been encrypted";
function encryptText(clearText: string | Uint8Array) {
@@ -1600,6 +1600,81 @@ describe("Cipher Service", () => {
});
});
describe("getAllDecrypted()", () => {
let mockSdkClient: any;
let mockCiphersSdk: any;
let mockVaultSdk: any;
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
mockCiphersSdk = {
list: jest.fn().mockResolvedValue({
successes: [mockSdkCipherView1, mockSdkCipherView2],
failures: [],
}),
};
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));
// Clear the decrypted cache to ensure we test the decrypt path
stateProvider.singleUser.getFake(mockUserId, DECRYPTED_CIPHERS).nextState({});
});
it("should use SDK to list and decrypt ciphers when feature flag is enabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(true);
const result = await cipherService.getAllDecrypted(mockUserId);
expect(mockSdkClient.take).toHaveBeenCalled();
expect(mockCiphersSdk.list).toHaveBeenCalled();
expect(result).toHaveLength(2);
expect(result[0]).toBeInstanceOf(CipherView);
expect(result[1]).toBeInstanceOf(CipherView);
});
it("should not call SDK when feature flag is disabled", async () => {
configService.getFeatureFlag
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
.mockResolvedValue(false);
// Just verify SDK is not called - don't test the full legacy path
// as it would require complex mocking of keyService observables
stateProvider.singleUser.getFake(mockUserId, ENCRYPTED_CIPHERS).nextState({});
try {
await cipherService.getAllDecrypted(mockUserId);
} catch {
// Expected to fail due to missing keyService mocks, but that's okay
// We just want to verify SDK wasn't called
}
expect(mockSdkClient.take).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.

View File

@@ -8,6 +8,7 @@ import {
firstValueFrom,
map,
Observable,
of,
Subject,
switchMap,
tap,
@@ -27,7 +28,7 @@ import { AutofillSettingsServiceAbstraction } from "../../autofill/services/auto
import { DomainSettingsService } from "../../autofill/services/domain-settings.service";
import { FeatureFlag } from "../../enums/feature-flag.enum";
import { EncryptService } from "../../key-management/crypto/abstractions/encrypt.service";
import { EncString } from "../../key-management/crypto/models/enc-string";
import { DECRYPT_ERROR, EncString } from "../../key-management/crypto/models/enc-string";
import { UriMatchStrategySetting } from "../../models/domain/domain-service";
import { ErrorResponse } from "../../models/response/error.response";
import { ListResponse } from "../../models/response/list.response";
@@ -468,6 +469,13 @@ export class CipherService implements CipherServiceAbstraction {
* @deprecated Use `cipherViews$` observable instead
*/
async getAllDecrypted(userId: UserId): Promise<CipherView[]> {
const useSdk = await this.configService.getFeatureFlag(
FeatureFlag.PM27632_SdkCipherCrudOperations,
);
if (useSdk) {
return this.getAllDecrypted_sdk(userId);
}
const decCiphers = await this.getDecryptedCiphers(userId);
if (decCiphers != null && decCiphers.length !== 0) {
await this.reindexCiphers(userId);
@@ -489,6 +497,44 @@ export class CipherService implements CipherServiceAbstraction {
return newDecCiphers;
}
private async getAllDecrypted_sdk(userId: UserId): Promise<CipherView[]> {
// Use SDK to list and decrypt all ciphers from state
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().list();
const successViews = decryptResult.successes.map((sdkCipherView: any) =>
CipherView.fromSdkCipherView(sdkCipherView),
);
const failureViews: CipherView[] = decryptResult.failures.map((failure) => {
const cipher = Cipher.fromSdkCipher(failure);
const cipherView = new CipherView(cipher);
cipherView.name = DECRYPT_ERROR;
cipherView.decryptionFailure = true;
return cipherView;
});
await this.setDecryptedCipherCache(successViews, userId);
await this.setFailedDecryptedCiphers(failureViews, userId);
return successViews;
}),
catchError((error: unknown) => {
this.logService.error(`Failed to list and decrypt ciphers: ${error}`);
return of([]);
}),
),
);
return result;
}
private async getDecryptedCiphers(userId: UserId) {
return Object.values(
await firstValueFrom(this.decryptedCiphersState(userId).state$.pipe(map((c) => c ?? {}))),
@@ -771,7 +817,7 @@ export class CipherService implements CipherServiceAbstraction {
}),
catchError((error: unknown) => {
this.logService.error(`Failed to list organization ciphers: ${error}`);
return [];
return of([]);
}),
),
);