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:
@@ -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.
|
||||
|
||||
@@ -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([]);
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user