mirror of
https://github.com/bitwarden/browser
synced 2026-01-26 22:33:44 +00:00
Merge branch 'vault/pm-30303/sdk-cipher-ops-delete' of https://github.com/bitwarden/clients into vault/pm-30304/cipher-sdk-get
This commit is contained in:
@@ -194,6 +194,7 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service"
|
||||
import { InternalSendService as InternalSendServiceAbstraction } from "@bitwarden/common/tools/send/services/send.service.abstraction";
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherSdkService } from "@bitwarden/common/vault/abstractions/cipher-sdk.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
@@ -211,6 +212,7 @@ import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DefaultCipherSdkService } from "@bitwarden/common/vault/services/cipher-sdk.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
import { CipherFileUploadService } from "@bitwarden/common/vault/services/file-upload/cipher-file-upload.service";
|
||||
@@ -367,6 +369,7 @@ export default class MainBackground {
|
||||
apiService: ApiServiceAbstraction;
|
||||
hibpApiService: HibpApiService;
|
||||
environmentService: BrowserEnvironmentService;
|
||||
cipherSdkService: CipherSdkService;
|
||||
cipherService: CipherServiceAbstraction;
|
||||
folderService: InternalFolderServiceAbstraction;
|
||||
userDecryptionOptionsService: InternalUserDecryptionOptionsServiceAbstraction;
|
||||
@@ -973,6 +976,8 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
);
|
||||
|
||||
this.cipherSdkService = new DefaultCipherSdkService(this.sdkService, this.logService);
|
||||
|
||||
this.cipherService = new CipherService(
|
||||
this.keyService,
|
||||
this.domainSettingsService,
|
||||
@@ -988,7 +993,7 @@ export default class MainBackground {
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
this.sdkService,
|
||||
this.cipherSdkService,
|
||||
);
|
||||
this.folderService = new FolderService(
|
||||
this.keyService,
|
||||
|
||||
@@ -147,11 +147,13 @@ import { SendService } from "@bitwarden/common/tools/send/services/send.service"
|
||||
import { UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherSdkService } from "@bitwarden/common/vault/abstractions/cipher-sdk.service";
|
||||
import { InternalFolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DefaultCipherSdkService } from "@bitwarden/common/vault/services/cipher-sdk.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
@@ -254,6 +256,7 @@ export class ServiceContainer {
|
||||
twoFactorApiService: TwoFactorApiService;
|
||||
hibpApiService: HibpApiService;
|
||||
environmentService: EnvironmentService;
|
||||
cipherSdkService: CipherSdkService;
|
||||
cipherService: CipherService;
|
||||
folderService: InternalFolderService;
|
||||
organizationUserApiService: OrganizationUserApiService;
|
||||
@@ -794,6 +797,8 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
);
|
||||
|
||||
this.cipherSdkService = new DefaultCipherSdkService(this.sdkService, this.logService);
|
||||
|
||||
this.cipherService = new CipherService(
|
||||
this.keyService,
|
||||
this.domainSettingsService,
|
||||
@@ -809,7 +814,7 @@ export class ServiceContainer {
|
||||
this.logService,
|
||||
this.cipherEncryptionService,
|
||||
this.messagingService,
|
||||
this.sdkService,
|
||||
this.cipherSdkService,
|
||||
);
|
||||
|
||||
this.cipherArchiveService = new DefaultCipherArchiveService(
|
||||
|
||||
@@ -303,6 +303,7 @@ import {
|
||||
import { CipherArchiveService } from "@bitwarden/common/vault/abstractions/cipher-archive.service";
|
||||
import { CipherEncryptionService } from "@bitwarden/common/vault/abstractions/cipher-encryption.service";
|
||||
import { CipherRiskService } from "@bitwarden/common/vault/abstractions/cipher-risk.service";
|
||||
import { CipherSdkService } from "@bitwarden/common/vault/abstractions/cipher-sdk.service";
|
||||
import { CipherService as CipherServiceAbstraction } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { CipherFileUploadService as CipherFileUploadServiceAbstraction } from "@bitwarden/common/vault/abstractions/file-upload/cipher-file-upload.service";
|
||||
import { FolderApiServiceAbstraction } from "@bitwarden/common/vault/abstractions/folder/folder-api.service.abstraction";
|
||||
@@ -321,6 +322,7 @@ import {
|
||||
CipherAuthorizationService,
|
||||
DefaultCipherAuthorizationService,
|
||||
} from "@bitwarden/common/vault/services/cipher-authorization.service";
|
||||
import { DefaultCipherSdkService } from "@bitwarden/common/vault/services/cipher-sdk.service";
|
||||
import { CipherService } from "@bitwarden/common/vault/services/cipher.service";
|
||||
import { DefaultCipherArchiveService } from "@bitwarden/common/vault/services/default-cipher-archive.service";
|
||||
import { DefaultCipherEncryptionService } from "@bitwarden/common/vault/services/default-cipher-encryption.service";
|
||||
@@ -590,6 +592,11 @@ const safeProviders: SafeProvider[] = [
|
||||
useClass: DefaultDomainSettingsService,
|
||||
deps: [StateProvider, PolicyServiceAbstraction, AccountService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherSdkService,
|
||||
useClass: DefaultCipherSdkService,
|
||||
deps: [SdkService, LogService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: CipherServiceAbstraction,
|
||||
useFactory: (
|
||||
@@ -607,7 +614,7 @@ const safeProviders: SafeProvider[] = [
|
||||
logService: LogService,
|
||||
cipherEncryptionService: CipherEncryptionService,
|
||||
messagingService: MessagingServiceAbstraction,
|
||||
sdkService: SdkService,
|
||||
cipherSdkService: CipherSdkService,
|
||||
) =>
|
||||
new CipherService(
|
||||
keyService,
|
||||
@@ -624,7 +631,7 @@ const safeProviders: SafeProvider[] = [
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
messagingService,
|
||||
sdkService,
|
||||
cipherSdkService,
|
||||
),
|
||||
deps: [
|
||||
KeyService,
|
||||
@@ -641,7 +648,7 @@ const safeProviders: SafeProvider[] = [
|
||||
LogService,
|
||||
CipherEncryptionService,
|
||||
MessagingServiceAbstraction,
|
||||
SdkService,
|
||||
CipherSdkService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
|
||||
109
libs/common/src/vault/abstractions/cipher-sdk.service.ts
Normal file
109
libs/common/src/vault/abstractions/cipher-sdk.service.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
|
||||
|
||||
/**
|
||||
* Service responsible for cipher operations using the SDK.
|
||||
*/
|
||||
export abstract class CipherSdkService {
|
||||
/**
|
||||
* Creates a new cipher on the server using the SDK.
|
||||
*
|
||||
* @param cipherView The cipher view to create
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param orgAdmin Whether this is an organization admin operation
|
||||
* @returns A promise that resolves to the created cipher view
|
||||
*/
|
||||
abstract createWithServer(
|
||||
cipherView: CipherView,
|
||||
userId: UserId,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView | undefined>;
|
||||
|
||||
/**
|
||||
* Updates a cipher on the server using the SDK.
|
||||
*
|
||||
* @param cipher The cipher view to update
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param originalCipherView The original cipher view before changes (optional, used for admin operations)
|
||||
* @param orgAdmin Whether this is an organization admin operation
|
||||
* @returns A promise that resolves to the updated cipher view
|
||||
*/
|
||||
abstract updateWithServer(
|
||||
cipher: CipherView,
|
||||
userId: UserId,
|
||||
originalCipherView?: CipherView,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView | undefined>;
|
||||
|
||||
/**
|
||||
* Deletes a cipher on the server using the SDK.
|
||||
*
|
||||
* @param id The cipher ID to delete
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param asAdmin Whether this is an organization admin operation
|
||||
* @returns A promise that resolves when the cipher is deleted
|
||||
*/
|
||||
abstract deleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Deletes multiple ciphers on the server using the SDK.
|
||||
*
|
||||
* @param ids The cipher IDs to delete
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param asAdmin Whether this is an organization admin operation
|
||||
* @param orgId The organization ID (required when asAdmin is true)
|
||||
* @returns A promise that resolves when the ciphers are deleted
|
||||
*/
|
||||
abstract deleteManyWithServer(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin?: boolean,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Soft deletes a cipher on the server using the SDK.
|
||||
*
|
||||
* @param id The cipher ID to soft delete
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param asAdmin Whether this is an organization admin operation
|
||||
* @returns A promise that resolves when the cipher is soft deleted
|
||||
*/
|
||||
abstract softDeleteWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Soft deletes multiple ciphers on the server using the SDK.
|
||||
*
|
||||
* @param ids The cipher IDs to soft delete
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param asAdmin Whether this is an organization admin operation
|
||||
* @param orgId The organization ID (required when asAdmin is true)
|
||||
* @returns A promise that resolves when the ciphers are soft deleted
|
||||
*/
|
||||
abstract softDeleteManyWithServer(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin?: boolean,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<void>;
|
||||
|
||||
/**
|
||||
* Restores a soft-deleted cipher on the server using the SDK.
|
||||
*
|
||||
* @param id The cipher ID to restore
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param asAdmin Whether this is an organization admin operation
|
||||
* @returns A promise that resolves when the cipher is restored
|
||||
*/
|
||||
abstract restoreWithServer(id: string, userId: UserId, asAdmin?: boolean): Promise<void>;
|
||||
|
||||
/**
|
||||
* Restores multiple soft-deleted ciphers on the server using the SDK.
|
||||
*
|
||||
* @param ids The cipher IDs to restore
|
||||
* @param userId The user ID to use for SDK client
|
||||
* @param orgId The organization ID (determines whether to use admin API)
|
||||
* @returns A promise that resolves when the ciphers are restored
|
||||
*/
|
||||
abstract restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise<void>;
|
||||
}
|
||||
@@ -353,4 +353,366 @@ describe("CipherView", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Note: These tests use jest.requireActual() because the file has jest.mock() calls
|
||||
// at the top that mock LoginView, FieldView, etc. Those mocks are needed for other tests
|
||||
// but interfere with these tests which need the real implementations.
|
||||
describe("toSdkCreateCipherRequest", () => {
|
||||
it("maps all properties correctly for a login cipher", () => {
|
||||
const { FieldView: RealFieldView } = jest.requireActual("./field.view");
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c";
|
||||
cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f";
|
||||
cipherView.collectionIds = ["b0473506-3c3c-4260-a734-dfaaf833ab6f"];
|
||||
cipherView.name = "Test Login";
|
||||
cipherView.notes = "Test notes";
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.favorite = true;
|
||||
cipherView.reprompt = CipherRepromptType.Password;
|
||||
|
||||
const field = new RealFieldView();
|
||||
field.name = "testField";
|
||||
field.value = "testValue";
|
||||
field.type = SdkFieldType.Text;
|
||||
cipherView.fields = [field];
|
||||
|
||||
cipherView.login = new RealLoginView();
|
||||
cipherView.login.username = "testuser";
|
||||
cipherView.login.password = "testpass";
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
expect(result.organizationId).toEqual(asUuid("000f2a6e-da5e-4726-87ed-1c5c77322c3c"));
|
||||
expect(result.folderId).toEqual(asUuid("41b22db4-8e2a-4ed2-b568-f1186c72922f"));
|
||||
expect(result.collectionIds).toEqual([asUuid("b0473506-3c3c-4260-a734-dfaaf833ab6f")]);
|
||||
expect(result.name).toBe("Test Login");
|
||||
expect(result.notes).toBe("Test notes");
|
||||
expect(result.favorite).toBe(true);
|
||||
expect(result.reprompt).toBe(CipherRepromptType.Password);
|
||||
expect(result.fields).toHaveLength(1);
|
||||
expect(result.fields![0]).toMatchObject({
|
||||
name: "testField",
|
||||
value: "testValue",
|
||||
type: SdkFieldType.Text,
|
||||
});
|
||||
expect(result.type).toHaveProperty("login");
|
||||
expect((result.type as any).login).toMatchObject({
|
||||
username: "testuser",
|
||||
password: "testpass",
|
||||
});
|
||||
});
|
||||
|
||||
it("handles undefined organizationId and folderId", () => {
|
||||
const { SecureNoteView: RealSecureNoteView } = jest.requireActual("./secure-note.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.type = CipherType.SecureNote;
|
||||
cipherView.secureNote = new RealSecureNoteView();
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
expect(result.organizationId).toBeUndefined();
|
||||
expect(result.folderId).toBeUndefined();
|
||||
expect(result.name).toBe("Test Cipher");
|
||||
});
|
||||
|
||||
it("handles empty collectionIds array", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.collectionIds = [];
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
expect(result.collectionIds).toEqual([]);
|
||||
});
|
||||
|
||||
it("defaults favorite to false when undefined", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.favorite = undefined as any;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
expect(result.favorite).toBe(false);
|
||||
});
|
||||
|
||||
it("defaults reprompt to None when undefined", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.reprompt = undefined as any;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
expect(result.reprompt).toBe(CipherRepromptType.None);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Login", CipherType.Login, "login.view", "LoginView"],
|
||||
["Card", CipherType.Card, "card.view", "CardView"],
|
||||
["Identity", CipherType.Identity, "identity.view", "IdentityView"],
|
||||
["SecureNote", CipherType.SecureNote, "secure-note.view", "SecureNoteView"],
|
||||
["SshKey", CipherType.SshKey, "ssh-key.view", "SshKeyView"],
|
||||
])(
|
||||
"creates correct type property for %s cipher",
|
||||
(typeName: string, cipherType: CipherType, moduleName: string, className: string) => {
|
||||
const module = jest.requireActual(`./${moduleName}`);
|
||||
const ViewClass = module[className];
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = `Test ${typeName}`;
|
||||
cipherView.type = cipherType;
|
||||
|
||||
// Set the appropriate view property
|
||||
const viewPropertyName = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
||||
(cipherView as any)[viewPropertyName] = new ViewClass();
|
||||
|
||||
const result = cipherView.toSdkCreateCipherRequest();
|
||||
|
||||
const typeKey = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
||||
expect(result.type).toHaveProperty(typeKey);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("toSdkUpdateCipherRequest", () => {
|
||||
it("maps all properties correctly for an update request", () => {
|
||||
const { FieldView: RealFieldView } = jest.requireActual("./field.view");
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||
cipherView.organizationId = "000f2a6e-da5e-4726-87ed-1c5c77322c3c";
|
||||
cipherView.folderId = "41b22db4-8e2a-4ed2-b568-f1186c72922f";
|
||||
cipherView.name = "Updated Login";
|
||||
cipherView.notes = "Updated notes";
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.favorite = true;
|
||||
cipherView.reprompt = CipherRepromptType.Password;
|
||||
cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z");
|
||||
cipherView.archivedDate = new Date("2022-01-03T12:00:00.000Z");
|
||||
cipherView.key = new EncString("cipher-key");
|
||||
|
||||
const mockField = new RealFieldView();
|
||||
mockField.name = "testField";
|
||||
mockField.value = "testValue";
|
||||
cipherView.fields = [mockField];
|
||||
|
||||
cipherView.login = new RealLoginView();
|
||||
cipherView.login.username = "testuser";
|
||||
|
||||
const result = cipherView.toSdkUpdateCipherRequest();
|
||||
|
||||
expect(result.id).toEqual(asUuid("0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602"));
|
||||
expect(result.organizationId).toEqual(asUuid("000f2a6e-da5e-4726-87ed-1c5c77322c3c"));
|
||||
expect(result.folderId).toEqual(asUuid("41b22db4-8e2a-4ed2-b568-f1186c72922f"));
|
||||
expect(result.name).toBe("Updated Login");
|
||||
expect(result.notes).toBe("Updated notes");
|
||||
expect(result.favorite).toBe(true);
|
||||
expect(result.reprompt).toBe(CipherRepromptType.Password);
|
||||
expect(result.revisionDate).toBe("2022-01-02T12:00:00.000Z");
|
||||
expect(result.archivedDate).toBe("2022-01-03T12:00:00.000Z");
|
||||
expect(result.fields).toHaveLength(1);
|
||||
expect(result.fields![0]).toMatchObject({
|
||||
name: "testField",
|
||||
value: "testValue",
|
||||
});
|
||||
expect(result.type).toHaveProperty("login");
|
||||
expect((result.type as any).login).toMatchObject({
|
||||
username: "testuser",
|
||||
});
|
||||
expect(result.key).toBeDefined();
|
||||
});
|
||||
|
||||
it("handles undefined optional properties", () => {
|
||||
const { SecureNoteView: RealSecureNoteView } = jest.requireActual("./secure-note.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.type = CipherType.SecureNote;
|
||||
cipherView.secureNote = new RealSecureNoteView();
|
||||
cipherView.revisionDate = new Date("2022-01-02T12:00:00.000Z");
|
||||
|
||||
const result = cipherView.toSdkUpdateCipherRequest();
|
||||
|
||||
expect(result.organizationId).toBeUndefined();
|
||||
expect(result.folderId).toBeUndefined();
|
||||
expect(result.archivedDate).toBeUndefined();
|
||||
expect(result.key).toBeUndefined();
|
||||
});
|
||||
|
||||
it("converts dates to ISO strings", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
cipherView.revisionDate = new Date("2022-05-15T10:30:00.000Z");
|
||||
cipherView.archivedDate = new Date("2022-06-20T14:45:00.000Z");
|
||||
|
||||
const result = cipherView.toSdkUpdateCipherRequest();
|
||||
|
||||
expect(result.revisionDate).toBe("2022-05-15T10:30:00.000Z");
|
||||
expect(result.archivedDate).toBe("2022-06-20T14:45:00.000Z");
|
||||
});
|
||||
|
||||
it("includes attachments when present", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
const { AttachmentView: RealAttachmentView } = jest.requireActual("./attachment.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
|
||||
const attachment1 = new RealAttachmentView();
|
||||
attachment1.id = "attachment-id-1";
|
||||
attachment1.fileName = "file1.txt";
|
||||
|
||||
const attachment2 = new RealAttachmentView();
|
||||
attachment2.id = "attachment-id-2";
|
||||
attachment2.fileName = "file2.pdf";
|
||||
|
||||
cipherView.attachments = [attachment1, attachment2];
|
||||
|
||||
const result = cipherView.toSdkUpdateCipherRequest();
|
||||
|
||||
expect(result.attachments).toHaveLength(2);
|
||||
});
|
||||
|
||||
test.each([
|
||||
["Login", CipherType.Login, "login.view", "LoginView"],
|
||||
["Card", CipherType.Card, "card.view", "CardView"],
|
||||
["Identity", CipherType.Identity, "identity.view", "IdentityView"],
|
||||
["SecureNote", CipherType.SecureNote, "secure-note.view", "SecureNoteView"],
|
||||
["SshKey", CipherType.SshKey, "ssh-key.view", "SshKeyView"],
|
||||
])(
|
||||
"creates correct type property for %s cipher",
|
||||
(typeName: string, cipherType: CipherType, moduleName: string, className: string) => {
|
||||
const module = jest.requireActual(`./${moduleName}`);
|
||||
const ViewClass = module[className];
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = "0a54d80c-14aa-4ef8-8c3a-7ea99ce5b602";
|
||||
cipherView.name = `Test ${typeName}`;
|
||||
cipherView.type = cipherType;
|
||||
|
||||
// Set the appropriate view property
|
||||
const viewPropertyName = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
||||
(cipherView as any)[viewPropertyName] = new ViewClass();
|
||||
|
||||
const result = cipherView.toSdkUpdateCipherRequest();
|
||||
|
||||
const typeKey = typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
||||
expect(result.type).toHaveProperty(typeKey);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("getSdkCipherViewType", () => {
|
||||
it("returns login type for Login cipher", () => {
|
||||
const { LoginView: RealLoginView } = jest.requireActual("./login.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.login = new RealLoginView();
|
||||
cipherView.login.username = "testuser";
|
||||
cipherView.login.password = "testpass";
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("login");
|
||||
expect((result as any).login).toMatchObject({
|
||||
username: "testuser",
|
||||
password: "testpass",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns card type for Card cipher", () => {
|
||||
const { CardView: RealCardView } = jest.requireActual("./card.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = CipherType.Card;
|
||||
cipherView.card = new RealCardView();
|
||||
cipherView.card.cardholderName = "John Doe";
|
||||
cipherView.card.number = "4111111111111111";
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("card");
|
||||
expect((result as any).card.cardholderName).toBe("John Doe");
|
||||
expect((result as any).card.number).toBe("4111111111111111");
|
||||
});
|
||||
|
||||
it("returns identity type for Identity cipher", () => {
|
||||
const { IdentityView: RealIdentityView } = jest.requireActual("./identity.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = CipherType.Identity;
|
||||
cipherView.identity = new RealIdentityView();
|
||||
cipherView.identity.firstName = "John";
|
||||
cipherView.identity.lastName = "Doe";
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("identity");
|
||||
expect((result as any).identity.firstName).toBe("John");
|
||||
expect((result as any).identity.lastName).toBe("Doe");
|
||||
});
|
||||
|
||||
it("returns secureNote type for SecureNote cipher", () => {
|
||||
const { SecureNoteView: RealSecureNoteView } = jest.requireActual("./secure-note.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = CipherType.SecureNote;
|
||||
cipherView.secureNote = new RealSecureNoteView();
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("secureNote");
|
||||
});
|
||||
|
||||
it("returns sshKey type for SshKey cipher", () => {
|
||||
const { SshKeyView: RealSshKeyView } = jest.requireActual("./ssh-key.view");
|
||||
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = CipherType.SshKey;
|
||||
cipherView.sshKey = new RealSshKeyView();
|
||||
cipherView.sshKey.privateKey = "privateKeyData";
|
||||
cipherView.sshKey.publicKey = "publicKeyData";
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("sshKey");
|
||||
expect((result as any).sshKey.privateKey).toBe("privateKeyData");
|
||||
expect((result as any).sshKey.publicKey).toBe("publicKeyData");
|
||||
});
|
||||
|
||||
it("defaults to empty login for unknown cipher type", () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.type = 999 as CipherType;
|
||||
|
||||
const result = cipherView.getSdkCipherViewType();
|
||||
|
||||
expect(result).toHaveProperty("login");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
534
libs/common/src/vault/services/cipher-sdk.service.spec.ts
Normal file
534
libs/common/src/vault/services/cipher-sdk.service.spec.ts
Normal file
@@ -0,0 +1,534 @@
|
||||
import { mock } from "jest-mock-extended";
|
||||
import { of } from "rxjs";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
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 { DefaultCipherSdkService } from "./cipher-sdk.service";
|
||||
|
||||
describe("DefaultCipherSdkService", () => {
|
||||
const sdkService = mock<SdkService>();
|
||||
const logService = mock<LogService>();
|
||||
const userId = "test-user-id" as UserId;
|
||||
const cipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b21" as OrganizationId;
|
||||
|
||||
let cipherSdkService: DefaultCipherSdkService;
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the SDK client chain for admin operations
|
||||
mockAdminSdk = {
|
||||
create: jest.fn(),
|
||||
edit: jest.fn(),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
soft_delete: jest.fn().mockResolvedValue(undefined),
|
||||
soft_delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
restore: jest.fn().mockResolvedValue(undefined),
|
||||
restore_many: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
create: jest.fn(),
|
||||
edit: jest.fn(),
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
soft_delete: jest.fn().mockResolvedValue(undefined),
|
||||
soft_delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
restore: jest.fn().mockResolvedValue(undefined),
|
||||
restore_many: jest.fn().mockResolvedValue(undefined),
|
||||
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));
|
||||
|
||||
cipherSdkService = new DefaultCipherSdkService(sdkService, logService);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("createWithServer()", () => {
|
||||
it("should create cipher using SDK when orgAdmin is false", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.name = "Test Cipher";
|
||||
cipherView.organizationId = orgId;
|
||||
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
mockCiphersSdk.create.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherSdkService.createWithServer(cipherView, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: cipherView.name,
|
||||
organizationId: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result?.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should create cipher using SDK admin API when orgAdmin is true", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.name = "Test Admin Cipher";
|
||||
cipherView.organizationId = orgId;
|
||||
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
mockAdminSdk.create.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherSdkService.createWithServer(cipherView, userId, true);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: cipherView.name,
|
||||
}),
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result?.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
|
||||
await expect(cipherSdkService.createWithServer(cipherView, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to create cipher"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
|
||||
mockCiphersSdk.create.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.createWithServer(cipherView, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to create cipher"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateWithServer()", () => {
|
||||
it("should update cipher using SDK when orgAdmin is false", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.name = "Updated Cipher";
|
||||
cipherView.organizationId = orgId;
|
||||
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
mockCiphersSdk.edit.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.edit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
name: cipherView.name,
|
||||
}),
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should update cipher using SDK admin API when orgAdmin is true", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.name = "Updated Admin Cipher";
|
||||
cipherView.organizationId = orgId;
|
||||
|
||||
const originalCipherView = new CipherView();
|
||||
originalCipherView.id = cipherId;
|
||||
originalCipherView.name = "Original Cipher";
|
||||
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
mockAdminSdk.edit.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherSdkService.updateWithServer(
|
||||
cipherView,
|
||||
userId,
|
||||
originalCipherView,
|
||||
true,
|
||||
);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.edit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
name: cipherView.name,
|
||||
}),
|
||||
originalCipherView.toSdkCipherView(),
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should update cipher using SDK admin API without originalCipherView", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.id = cipherId;
|
||||
cipherView.type = CipherType.Login;
|
||||
cipherView.name = "Updated Admin Cipher";
|
||||
cipherView.organizationId = orgId;
|
||||
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
mockAdminSdk.edit.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherSdkService.updateWithServer(cipherView, userId, undefined, true);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.edit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
name: cipherView.name,
|
||||
}),
|
||||
expect.anything(), // Empty CipherView - timestamps vary so we just verify it was called
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
|
||||
await expect(
|
||||
cipherSdkService.updateWithServer(cipherView, userId, undefined, false),
|
||||
).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to update cipher"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
const cipherView = new CipherView();
|
||||
cipherView.name = "Test Cipher";
|
||||
|
||||
mockCiphersSdk.edit.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(
|
||||
cipherSdkService.updateWithServer(cipherView, userId, undefined, false),
|
||||
).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to update cipher"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteWithServer()", () => {
|
||||
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
|
||||
it("should delete cipher using SDK when asAdmin is false", async () => {
|
||||
await cipherSdkService.deleteWithServer(testCipherId, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete cipher using SDK admin API when asAdmin is true", async () => {
|
||||
await cipherSdkService.deleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.delete).toHaveBeenCalledWith(testCipherId);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(cipherSdkService.deleteWithServer(testCipherId, userId)).rejects.toThrow(
|
||||
"SDK not available",
|
||||
);
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete cipher"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.delete.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.deleteWithServer(testCipherId, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete cipher"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteManyWithServer()", () => {
|
||||
const testCipherIds = [
|
||||
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
|
||||
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
|
||||
];
|
||||
|
||||
it("should delete multiple ciphers using SDK when asAdmin is false", async () => {
|
||||
await cipherSdkService.deleteManyWithServer(testCipherIds, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.delete_many).toHaveBeenCalledWith(testCipherIds);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
|
||||
await cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
|
||||
});
|
||||
|
||||
it("should throw error when asAdmin is true but orgId is missing", async () => {
|
||||
await expect(
|
||||
cipherSdkService.deleteManyWithServer(testCipherIds, userId, true, undefined),
|
||||
).rejects.toThrow("Organization ID is required for admin delete.");
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(cipherSdkService.deleteManyWithServer(testCipherIds, userId)).rejects.toThrow(
|
||||
"SDK not available",
|
||||
);
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete multiple ciphers"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.delete_many.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.deleteManyWithServer(testCipherIds, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to delete multiple ciphers"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("softDeleteWithServer()", () => {
|
||||
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
|
||||
it("should soft delete cipher using SDK when asAdmin is false", async () => {
|
||||
await cipherSdkService.softDeleteWithServer(testCipherId, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should soft delete cipher using SDK admin API when asAdmin is true", async () => {
|
||||
await cipherSdkService.softDeleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(cipherSdkService.softDeleteWithServer(testCipherId, userId)).rejects.toThrow(
|
||||
"SDK not available",
|
||||
);
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to soft delete cipher"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.soft_delete.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.softDeleteWithServer(testCipherId, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to soft delete cipher"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("softDeleteManyWithServer()", () => {
|
||||
const testCipherIds = [
|
||||
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
|
||||
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
|
||||
];
|
||||
|
||||
it("should soft delete multiple ciphers using SDK when asAdmin is false", async () => {
|
||||
await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should soft delete multiple ciphers using SDK admin API when asAdmin is true", async () => {
|
||||
await cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
|
||||
});
|
||||
|
||||
it("should throw error when asAdmin is true but orgId is missing", async () => {
|
||||
await expect(
|
||||
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId, true, undefined),
|
||||
).rejects.toThrow("Organization ID is required for admin soft delete.");
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(
|
||||
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId),
|
||||
).rejects.toThrow("SDK not available");
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to soft delete multiple ciphers"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.soft_delete_many.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(
|
||||
cipherSdkService.softDeleteManyWithServer(testCipherIds, userId),
|
||||
).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to soft delete multiple ciphers"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restoreWithServer()", () => {
|
||||
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
|
||||
it("should restore cipher using SDK when asAdmin is false", async () => {
|
||||
await cipherSdkService.restoreWithServer(testCipherId, userId, false);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.restore).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should restore cipher using SDK admin API when asAdmin is true", async () => {
|
||||
await cipherSdkService.restoreWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.restore).toHaveBeenCalledWith(testCipherId);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(cipherSdkService.restoreWithServer(testCipherId, userId)).rejects.toThrow(
|
||||
"SDK not available",
|
||||
);
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to restore cipher"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.restore.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.restoreWithServer(testCipherId, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to restore cipher"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("restoreManyWithServer()", () => {
|
||||
const testCipherIds = [
|
||||
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
|
||||
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
|
||||
];
|
||||
|
||||
it("should restore multiple ciphers using SDK when orgId is not provided", async () => {
|
||||
await cipherSdkService.restoreManyWithServer(testCipherIds, userId);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.restore_many).toHaveBeenCalledWith(testCipherIds);
|
||||
expect(mockCiphersSdk.admin).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should restore multiple ciphers using SDK admin API when orgId is provided", async () => {
|
||||
const orgIdString = orgId as string;
|
||||
await cipherSdkService.restoreManyWithServer(testCipherIds, userId, orgIdString);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockVaultSdk.ciphers).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.restore_many).toHaveBeenCalledWith(testCipherIds, orgIdString);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK client is not available", async () => {
|
||||
sdkService.userClient$.mockReturnValue(of(null));
|
||||
|
||||
await expect(cipherSdkService.restoreManyWithServer(testCipherIds, userId)).rejects.toThrow(
|
||||
"SDK not available",
|
||||
);
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to restore multiple ciphers"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error and log when SDK throws an error", async () => {
|
||||
mockCiphersSdk.restore_many.mockRejectedValue(new Error("SDK error"));
|
||||
|
||||
await expect(cipherSdkService.restoreManyWithServer(testCipherIds, userId)).rejects.toThrow();
|
||||
expect(logService.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Failed to restore multiple ciphers"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
263
libs/common/src/vault/services/cipher-sdk.service.ts
Normal file
263
libs/common/src/vault/services/cipher-sdk.service.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
import { firstValueFrom, switchMap, catchError } from "rxjs";
|
||||
|
||||
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 { CipherSdkService } from "../abstractions/cipher-sdk.service";
|
||||
|
||||
export class DefaultCipherSdkService implements CipherSdkService {
|
||||
constructor(
|
||||
private sdkService: SdkService,
|
||||
private logService: LogService,
|
||||
) {}
|
||||
|
||||
async createWithServer(
|
||||
cipherView: CipherView,
|
||||
userId: UserId,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView | undefined> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
const sdkCreateRequest = cipherView.toSdkCreateCipherRequest();
|
||||
let result: SdkCipherView;
|
||||
if (orgAdmin) {
|
||||
result = await ref.value.vault().ciphers().admin().create(sdkCreateRequest);
|
||||
} else {
|
||||
result = await ref.value.vault().ciphers().create(sdkCreateRequest);
|
||||
}
|
||||
return CipherView.fromSdkCipherView(result);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to create cipher: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async updateWithServer(
|
||||
cipher: CipherView,
|
||||
userId: UserId,
|
||||
originalCipherView?: CipherView,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView | undefined> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
const sdkUpdateRequest = cipher.toSdkUpdateCipherRequest();
|
||||
let result: SdkCipherView;
|
||||
if (orgAdmin) {
|
||||
result = await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.edit(
|
||||
sdkUpdateRequest,
|
||||
originalCipherView?.toSdkCipherView() || new CipherView().toSdkCipherView(),
|
||||
);
|
||||
} else {
|
||||
result = await ref.value.vault().ciphers().edit(sdkUpdateRequest);
|
||||
}
|
||||
return CipherView.fromSdkCipherView(result);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to update cipher: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().delete(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().delete(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete cipher: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async deleteManyWithServer(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
if (orgId == null) {
|
||||
throw new Error("Organization ID is required for admin delete.");
|
||||
}
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.delete_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.delete_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete multiple ciphers: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async softDeleteWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().soft_delete(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().soft_delete(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to soft delete cipher: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async softDeleteManyWithServer(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
if (orgId == null) {
|
||||
throw new Error("Organization ID is required for admin soft delete.");
|
||||
}
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.soft_delete_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.soft_delete_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to soft delete multiple ciphers: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async restoreWithServer(id: string, userId: UserId, asAdmin = false): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().restore(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().restore(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to restore cipher: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async restoreManyWithServer(ids: string[], userId: UserId, orgId?: string): Promise<void> {
|
||||
return await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
|
||||
// No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
|
||||
// The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
|
||||
if (orgId) {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.restore_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.restore_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to restore multiple ciphers: ${error}`);
|
||||
throw error;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,6 @@ import { EncString } from "../../key-management/crypto/models/enc-string";
|
||||
import { UriMatchStrategy } from "../../models/domain/domain-service";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { SdkService } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
import { SymmetricCryptoKey } from "../../platform/models/domain/symmetric-crypto-key";
|
||||
@@ -29,6 +28,7 @@ import { ContainerService } from "../../platform/services/container.service";
|
||||
import { CipherId, UserId, OrganizationId, CollectionId } from "../../types/guid";
|
||||
import { CipherKey, OrgKey, UserKey } from "../../types/key";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherSdkService } from "../abstractions/cipher-sdk.service";
|
||||
import { EncryptionContext } from "../abstractions/cipher.service";
|
||||
import { CipherFileUploadService } from "../abstractions/file-upload/cipher-file-upload.service";
|
||||
import { SearchService } from "../abstractions/search.service";
|
||||
@@ -110,7 +110,7 @@ describe("Cipher Service", () => {
|
||||
const stateProvider = new FakeStateProvider(accountService);
|
||||
const cipherEncryptionService = mock<CipherEncryptionService>();
|
||||
const messageSender = mock<MessageSender>();
|
||||
const sdkService = mock<SdkService>();
|
||||
const cipherSdkService = mock<CipherSdkService>();
|
||||
|
||||
const userId = "TestUserId" as UserId;
|
||||
const orgId = "4ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b21" as OrganizationId;
|
||||
@@ -147,7 +147,7 @@ describe("Cipher Service", () => {
|
||||
logService,
|
||||
cipherEncryptionService,
|
||||
messageSender,
|
||||
sdkService,
|
||||
cipherSdkService,
|
||||
);
|
||||
|
||||
encryptionContext = { cipher: new Cipher(cipherData), encryptedFor: userId };
|
||||
@@ -210,48 +210,11 @@ describe("Cipher Service", () => {
|
||||
});
|
||||
|
||||
describe("createWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
let sdkTestCipher: Cipher;
|
||||
let sdkTestCipherData: CipherData;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock encrypt to return encryptionContext for legacy path
|
||||
jest.spyOn(cipherService, "encrypt").mockResolvedValue(encryptionContext);
|
||||
// Mock decrypt to return cipherView for result
|
||||
jest.spyOn(cipherService, "decrypt").mockImplementation(async (cipher) => {
|
||||
return new CipherView(cipher);
|
||||
});
|
||||
|
||||
// Create cipher data with valid UUIDs for SDK tests
|
||||
sdkTestCipherData = cipherData;
|
||||
sdkTestCipher = new Cipher(sdkTestCipherData);
|
||||
|
||||
// Mock the SDK client chain - define mockAdminSdk first before referencing it
|
||||
mockAdminSdk = {
|
||||
create: jest.fn(),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
create: jest.fn(),
|
||||
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.postCipherAdmin when orgAdmin param is true and the cipher orgId != null", async () => {
|
||||
@@ -267,7 +230,6 @@ describe("Cipher Service", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(expectedObj);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.postCipher when orgAdmin param is true and the cipher orgId is null", async () => {
|
||||
@@ -284,7 +246,6 @@ describe("Cipher Service", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(expectedObj);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.postCipherCreate if collectionsIds != null", async () => {
|
||||
@@ -301,7 +262,6 @@ describe("Cipher Service", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(expectedObj);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.postCipher when orgAdmin and collectionIds logic is false", async () => {
|
||||
@@ -317,103 +277,41 @@ describe("Cipher Service", () => {
|
||||
|
||||
expect(spy).toHaveBeenCalled();
|
||||
expect(spy).toHaveBeenCalledWith(expectedObj);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use SDK to create cipher when feature flag is enabled", async () => {
|
||||
it("should delegate to cipherSdkService when feature flag is enabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const cipherView = new CipherView(sdkTestCipher);
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
const cipherView = new CipherView(encryptionContext.cipher);
|
||||
const expectedResult = new CipherView(encryptionContext.cipher);
|
||||
|
||||
// Mock SDK create to return a cipher view
|
||||
mockCiphersSdk.create.mockResolvedValue(mockSdkCipherView);
|
||||
const cipherSdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "createWithServer")
|
||||
.mockResolvedValue(expectedResult);
|
||||
|
||||
const clearCacheSpy = jest.spyOn(cipherService, "clearCache");
|
||||
const apiSpy = jest.spyOn(apiService, "postCipher");
|
||||
|
||||
const result = await cipherService.createWithServer(cipherView, userId);
|
||||
|
||||
expect(cipherSdkServiceSpy).toHaveBeenCalledWith(cipherView, userId, undefined);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCiphersSdk.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: cipherView.name,
|
||||
organizationId: expect.anything(),
|
||||
}),
|
||||
);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should use SDK to create admin cipher when feature flag is enabled and admin flag is passed", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const cipherView = new CipherView(sdkTestCipher);
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
|
||||
// Mock SDK admin create to return a cipher view
|
||||
mockAdminSdk.create.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "postCipherAdmin");
|
||||
const result = await cipherService.createWithServer(cipherView, userId, true);
|
||||
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.create).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
name: cipherView.name,
|
||||
}),
|
||||
);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock encrypt to return encryptionContext for legacy path
|
||||
jest.spyOn(cipherService, "encrypt").mockResolvedValue(encryptionContext);
|
||||
// Mock decrypt to return cipherView for result
|
||||
jest.spyOn(cipherService, "decrypt").mockImplementation(async (cipher) => {
|
||||
return new CipherView(cipher);
|
||||
});
|
||||
// Mock upsert to return the cipher data
|
||||
jest.spyOn(cipherService, "upsert").mockResolvedValue({
|
||||
[cipherData.id as CipherId]: cipherData,
|
||||
});
|
||||
|
||||
// Mock the SDK client chain for admin operations
|
||||
mockAdminSdk = {
|
||||
edit: jest.fn(),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
edit: jest.fn(),
|
||||
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.putCipherAdmin when orgAdmin param is true", async () => {
|
||||
@@ -421,7 +319,6 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(false);
|
||||
|
||||
// Create a fresh cipher with organizationId for this test
|
||||
const testCipher = new Cipher(cipherData);
|
||||
testCipher.organizationId = orgId;
|
||||
const testContext = { cipher: testCipher, encryptedFor: userId };
|
||||
@@ -470,47 +367,47 @@ describe("Cipher Service", () => {
|
||||
expect(spy).toHaveBeenCalledWith(encryptionContext.cipher.id, expectedObj);
|
||||
});
|
||||
|
||||
it("should use SDK to update cipher when feature flag is enabled", async () => {
|
||||
it("should delegate to cipherSdkService when feature flag is enabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const testCipher = new Cipher(cipherData);
|
||||
const cipherView = new CipherView(testCipher);
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
const expectedResult = new CipherView(testCipher);
|
||||
|
||||
// Mock SDK edit to return a cipher view
|
||||
mockCiphersSdk.edit.mockResolvedValue(mockSdkCipherView);
|
||||
const cipherSdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "updateWithServer")
|
||||
.mockResolvedValue(expectedResult);
|
||||
|
||||
const clearCacheSpy = jest.spyOn(cipherService, "clearCache");
|
||||
const apiSpy = jest.spyOn(apiService, "putCipher");
|
||||
|
||||
const result = await cipherService.updateWithServer(cipherView, userId);
|
||||
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCiphersSdk.edit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
name: cipherView.name,
|
||||
}),
|
||||
);
|
||||
expect(cipherSdkServiceSpy).toHaveBeenCalledWith(cipherView, userId, undefined, undefined);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
expect(result.name).toBe(cipherView.name);
|
||||
});
|
||||
|
||||
it("should use SDK admin API when orgAdmin is true", async () => {
|
||||
it("should delegate to cipherSdkService with orgAdmin when feature flag is enabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
// sdkTestCipherData already has a valid organizationId, use it directly
|
||||
const testCipher = new Cipher(cipherData);
|
||||
const cipherView = new CipherView(testCipher);
|
||||
const originalCipherView = new CipherView(testCipher);
|
||||
const mockSdkCipherView = cipherView.toSdkCipherView();
|
||||
const expectedResult = new CipherView(testCipher);
|
||||
|
||||
const cipherSdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "updateWithServer")
|
||||
.mockResolvedValue(expectedResult);
|
||||
|
||||
const clearCacheSpy = jest.spyOn(cipherService, "clearCache");
|
||||
const apiSpy = jest.spyOn(apiService, "putCipherAdmin");
|
||||
|
||||
// Mock SDK admin edit to return a cipher view
|
||||
mockAdminSdk.edit.mockResolvedValue(mockSdkCipherView);
|
||||
|
||||
const result = await cipherService.updateWithServer(
|
||||
cipherView,
|
||||
userId,
|
||||
@@ -518,16 +415,14 @@ describe("Cipher Service", () => {
|
||||
true,
|
||||
);
|
||||
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkService.userClient$).toHaveBeenCalledWith(userId);
|
||||
expect(mockCiphersSdk.admin).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.edit).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: expect.anything(),
|
||||
name: cipherView.name,
|
||||
}),
|
||||
originalCipherView.toSdkCipherView(),
|
||||
expect(cipherSdkServiceSpy).toHaveBeenCalledWith(
|
||||
cipherView,
|
||||
userId,
|
||||
originalCipherView,
|
||||
true,
|
||||
);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
expect(result).toBeInstanceOf(CipherView);
|
||||
});
|
||||
});
|
||||
@@ -1115,38 +1010,8 @@ describe("Cipher Service", () => {
|
||||
});
|
||||
|
||||
describe("deleteWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the SDK client chain for delete operations
|
||||
mockAdminSdk = {
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
delete: jest.fn().mockResolvedValue(undefined),
|
||||
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.deleteCipher when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
@@ -1157,7 +1022,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.deleteWithServer(testCipherId, userId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.deleteCipherAdmin when feature flag is disabled and asAdmin is true", async () => {
|
||||
@@ -1170,7 +1034,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.deleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use SDK to delete cipher when feature flag is enabled", async () => {
|
||||
@@ -1178,14 +1041,14 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "deleteCipher");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "deleteWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.deleteWithServer(testCipherId, userId);
|
||||
await cipherService.deleteWithServer(testCipherId, userId, false);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, false);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
@@ -1194,54 +1057,24 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "deleteCipherAdmin");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "deleteWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.deleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, true);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("deleteManyWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
const testCipherIds = [
|
||||
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
|
||||
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the SDK client chain for delete many operations
|
||||
mockAdminSdk = {
|
||||
delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
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.deleteManyCiphers when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
@@ -1252,7 +1085,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.deleteManyWithServer(testCipherIds, userId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalled();
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.deleteManyCiphersAdmin when feature flag is disabled and asAdmin is true", async () => {
|
||||
@@ -1265,7 +1097,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.deleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalled();
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use SDK to delete multiple ciphers when feature flag is enabled", async () => {
|
||||
@@ -1273,14 +1104,14 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "deleteManyCiphers");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "deleteManyWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.deleteManyWithServer(testCipherIds, userId);
|
||||
await cipherService.deleteManyWithServer(testCipherIds, userId, false);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.delete_many).toHaveBeenCalledWith(testCipherIds);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, false, undefined);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
@@ -1289,51 +1120,21 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "deleteManyCiphersAdmin");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "deleteManyWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.deleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, true, orgId);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("softDeleteWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
const testCipherId = "5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId;
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the SDK client chain for soft delete operations
|
||||
mockAdminSdk = {
|
||||
soft_delete: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
soft_delete: jest.fn().mockResolvedValue(undefined),
|
||||
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.putDeleteCipher when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
@@ -1344,7 +1145,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.softDeleteWithServer(testCipherId, userId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.putDeleteCipherAdmin when feature flag is disabled and asAdmin is true", async () => {
|
||||
@@ -1357,7 +1157,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.softDeleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith(testCipherId);
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use SDK to soft delete cipher when feature flag is enabled", async () => {
|
||||
@@ -1365,14 +1164,14 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "putDeleteCipher");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "softDeleteWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.softDeleteWithServer(testCipherId, userId);
|
||||
await cipherService.softDeleteWithServer(testCipherId, userId, false);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, false);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
@@ -1381,54 +1180,24 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "putDeleteCipherAdmin");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "softDeleteWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.softDeleteWithServer(testCipherId, userId, true);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.soft_delete).toHaveBeenCalledWith(testCipherId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherId, userId, true);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe("softDeleteManyWithServer()", () => {
|
||||
let mockSdkClient: any;
|
||||
let mockCiphersSdk: any;
|
||||
let mockAdminSdk: any;
|
||||
let mockVaultSdk: any;
|
||||
const testCipherIds = [
|
||||
"5ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b22" as CipherId,
|
||||
"6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CipherId,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock the SDK client chain for soft delete many operations
|
||||
mockAdminSdk = {
|
||||
soft_delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
mockCiphersSdk = {
|
||||
soft_delete_many: jest.fn().mockResolvedValue(undefined),
|
||||
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.putDeleteManyCiphers when feature flag is disabled", async () => {
|
||||
configService.getFeatureFlag
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
@@ -1439,7 +1208,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.softDeleteManyWithServer(testCipherIds, userId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalled();
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should call apiService.putDeleteManyCiphersAdmin when feature flag is disabled and asAdmin is true", async () => {
|
||||
@@ -1454,7 +1222,6 @@ describe("Cipher Service", () => {
|
||||
await cipherService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalled();
|
||||
expect(mockSdkClient.take).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should use SDK to soft delete multiple ciphers when feature flag is enabled", async () => {
|
||||
@@ -1462,14 +1229,14 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "putDeleteManyCiphers");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "softDeleteManyWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.softDeleteManyWithServer(testCipherIds, userId);
|
||||
await cipherService.softDeleteManyWithServer(testCipherIds, userId, false);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockCiphersSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, false, undefined);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
|
||||
@@ -1478,14 +1245,14 @@ describe("Cipher Service", () => {
|
||||
.calledWith(FeatureFlag.PM27632_SdkCipherCrudOperations)
|
||||
.mockResolvedValue(true);
|
||||
|
||||
const apiSpy = jest.spyOn(apiService, "putDeleteManyCiphersAdmin");
|
||||
const sdkServiceSpy = jest
|
||||
.spyOn(cipherSdkService, "softDeleteManyWithServer")
|
||||
.mockResolvedValue(undefined);
|
||||
const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache");
|
||||
|
||||
await cipherService.softDeleteManyWithServer(testCipherIds, userId, true, orgId);
|
||||
|
||||
expect(mockSdkClient.take).toHaveBeenCalled();
|
||||
expect(mockAdminSdk.soft_delete_many).toHaveBeenCalledWith(testCipherIds, orgId);
|
||||
expect(apiSpy).not.toHaveBeenCalled();
|
||||
expect(sdkServiceSpy).toHaveBeenCalledWith(testCipherIds, userId, true, orgId);
|
||||
expect(clearCacheSpy).toHaveBeenCalledWith(userId);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
EMPTY,
|
||||
filter,
|
||||
firstValueFrom,
|
||||
map,
|
||||
@@ -20,7 +18,7 @@ import { MessageSender } from "@bitwarden/common/platform/messaging";
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { CipherListView, CipherView as SdkCipherView } from "@bitwarden/sdk-internal";
|
||||
import { CipherListView } from "@bitwarden/sdk-internal";
|
||||
|
||||
import { ApiService } from "../../abstractions/api.service";
|
||||
import { AccountService } from "../../auth/abstractions/account.service";
|
||||
@@ -35,7 +33,7 @@ import { ListResponse } from "../../models/response/list.response";
|
||||
import { View } from "../../models/view/view";
|
||||
import { ConfigService } from "../../platform/abstractions/config/config.service";
|
||||
import { I18nService } from "../../platform/abstractions/i18n.service";
|
||||
import { asUuid, SdkService, uuidAsString } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { uuidAsString } from "../../platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "../../platform/misc/utils";
|
||||
import Domain from "../../platform/models/domain/domain-base";
|
||||
import { EncArrayBuffer } from "../../platform/models/domain/enc-array-buffer";
|
||||
@@ -45,6 +43,7 @@ import { CipherId, CollectionId, OrganizationId, UserId } from "../../types/guid
|
||||
import { OrgKey, UserKey } from "../../types/key";
|
||||
import { filterOutNullish, perUserCache$ } from "../../vault/utils/observable-utilities";
|
||||
import { CipherEncryptionService } from "../abstractions/cipher-encryption.service";
|
||||
import { CipherSdkService } from "../abstractions/cipher-sdk.service";
|
||||
import {
|
||||
CipherService as CipherServiceAbstraction,
|
||||
EncryptionContext,
|
||||
@@ -123,7 +122,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
private logService: LogService,
|
||||
private cipherEncryptionService: CipherEncryptionService,
|
||||
private messageSender: MessageSender,
|
||||
private sdkService: SdkService,
|
||||
private cipherSdkService: CipherSdkService,
|
||||
) {}
|
||||
|
||||
localData$(userId: UserId): Observable<Record<CipherId, LocalData>> {
|
||||
@@ -1015,45 +1014,30 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
userId: UserId,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView> {
|
||||
const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||
const useSdk = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
|
||||
if (sdkCipherEncryptionEnabled) {
|
||||
return (await this.createWithServer_sdk(cipherView, userId, orgAdmin)) || new CipherView();
|
||||
} else {
|
||||
const encrypted = await this.encrypt(cipherView, userId);
|
||||
const result = await this.createWithServer_legacy(encrypted, orgAdmin);
|
||||
return await this.decrypt(result, userId);
|
||||
if (useSdk) {
|
||||
return (
|
||||
(await this.createWithServerUsingSdk(cipherView, userId, orgAdmin)) || new CipherView()
|
||||
);
|
||||
}
|
||||
|
||||
const encrypted = await this.encrypt(cipherView, userId);
|
||||
const result = await this.createWithServer_legacy(encrypted, orgAdmin);
|
||||
return await this.decrypt(result, userId);
|
||||
}
|
||||
|
||||
private async createWithServer_sdk(
|
||||
private async createWithServerUsingSdk(
|
||||
cipherView: CipherView,
|
||||
userId: UserId,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView | void> {
|
||||
const resultCipherView = await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
const sdkCreateRequest = cipherView.toSdkCreateCipherRequest();
|
||||
let result: SdkCipherView;
|
||||
if (orgAdmin) {
|
||||
result = await ref.value.vault().ciphers().admin().create(sdkCreateRequest);
|
||||
} else {
|
||||
result = await ref.value.vault().ciphers().create(sdkCreateRequest);
|
||||
}
|
||||
return CipherView.fromSdkCipherView(result);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to create cipher: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
const resultCipherView = await this.cipherSdkService.createWithServer(
|
||||
cipherView,
|
||||
userId,
|
||||
orgAdmin,
|
||||
);
|
||||
await this.clearCache(userId);
|
||||
return resultCipherView;
|
||||
@@ -1091,51 +1075,31 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
originalCipherView?: CipherView,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView> {
|
||||
const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag(
|
||||
const useSdk = await this.configService.getFeatureFlag(
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
|
||||
if (sdkCipherEncryptionEnabled) {
|
||||
return await this.updateWithServer_sdk(cipherView, userId, originalCipherView, orgAdmin);
|
||||
} else {
|
||||
const encrypted = await this.encrypt(cipherView, userId);
|
||||
const updatedCipher = await this.updateWithServer_legacy(encrypted, orgAdmin);
|
||||
const updatedCipherView = this.decrypt(updatedCipher, userId);
|
||||
return updatedCipherView;
|
||||
if (useSdk) {
|
||||
return await this.updateWithServerUsingSdk(cipherView, userId, originalCipherView, orgAdmin);
|
||||
}
|
||||
|
||||
const encrypted = await this.encrypt(cipherView, userId);
|
||||
const updatedCipher = await this.updateWithServer_legacy(encrypted, orgAdmin);
|
||||
const updatedCipherView = await this.decrypt(updatedCipher, userId);
|
||||
return updatedCipherView;
|
||||
}
|
||||
|
||||
async updateWithServer_sdk(
|
||||
async updateWithServerUsingSdk(
|
||||
cipher: CipherView,
|
||||
userId: UserId,
|
||||
originalCipherView?: CipherView,
|
||||
orgAdmin?: boolean,
|
||||
): Promise<CipherView> {
|
||||
const resultCipherView = await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
const sdkUpdateRequest = cipher.toSdkUpdateCipherRequest();
|
||||
let result: SdkCipherView;
|
||||
if (orgAdmin) {
|
||||
result = await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.edit(sdkUpdateRequest, originalCipherView?.toSdkCipherView());
|
||||
} else {
|
||||
result = await ref.value.vault().ciphers().edit(sdkUpdateRequest);
|
||||
}
|
||||
return CipherView.fromSdkCipherView(result);
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to update cipher: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
const resultCipherView = await this.cipherSdkService.updateWithServer(
|
||||
cipher,
|
||||
userId,
|
||||
originalCipherView,
|
||||
orgAdmin,
|
||||
);
|
||||
await this.clearCache(userId);
|
||||
return resultCipherView;
|
||||
@@ -1535,7 +1499,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return this.deleteWithServer_sdk(id, userId, asAdmin);
|
||||
return this.deleteWithServerUsingSdk(id, userId, asAdmin);
|
||||
}
|
||||
|
||||
if (asAdmin) {
|
||||
@@ -1547,26 +1511,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.delete(id, userId);
|
||||
}
|
||||
|
||||
private async deleteWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().delete(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().delete(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete cipher: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
private async deleteWithServerUsingSdk(
|
||||
id: string,
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
): Promise<any> {
|
||||
await this.cipherSdkService.deleteWithServer(id, userId, asAdmin);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
@@ -1580,7 +1530,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return this.deleteManyWithServer_sdk(ids, userId, asAdmin, orgId);
|
||||
return this.deleteManyWithServerUsingSdk(ids, userId, asAdmin, orgId);
|
||||
}
|
||||
|
||||
const request = new CipherBulkDeleteRequest(ids);
|
||||
@@ -1592,44 +1542,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.delete(ids, userId);
|
||||
}
|
||||
|
||||
private async deleteManyWithServer_sdk(
|
||||
private async deleteManyWithServerUsingSdk(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<any> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
if (orgId == null) {
|
||||
throw new Error("Organization ID is required for admin delete.");
|
||||
}
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.delete_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.delete_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to delete multiple ciphers: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await this.cipherSdkService.deleteManyWithServer(ids, userId, asAdmin, orgId);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
@@ -1796,7 +1715,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return this.softDeleteWithServer_sdk(id, userId, asAdmin);
|
||||
return this.softDeleteWithServerUsingSdk(id, userId, asAdmin);
|
||||
}
|
||||
|
||||
if (asAdmin) {
|
||||
@@ -1808,26 +1727,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.softDelete(id, userId);
|
||||
}
|
||||
|
||||
async softDeleteWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().soft_delete(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().soft_delete(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to soft delete cipher: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
private async softDeleteWithServerUsingSdk(
|
||||
id: string,
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
): Promise<any> {
|
||||
await this.cipherSdkService.softDeleteWithServer(id, userId, asAdmin);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
@@ -1841,7 +1746,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return this.softDeleteManyWithServer_sdk(ids, userId, asAdmin, orgId);
|
||||
return this.softDeleteManyWithServerUsingSdk(ids, userId, asAdmin, orgId);
|
||||
}
|
||||
|
||||
const request = new CipherBulkDeleteRequest(ids);
|
||||
@@ -1854,41 +1759,13 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.softDelete(ids, userId);
|
||||
}
|
||||
|
||||
async softDeleteManyWithServer_sdk(
|
||||
private async softDeleteManyWithServerUsingSdk(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
orgId?: OrganizationId,
|
||||
): Promise<any> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.soft_delete_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.soft_delete_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to soft delete multiple ciphers: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await this.cipherSdkService.softDeleteManyWithServer(ids, userId, asAdmin, orgId);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
@@ -1930,7 +1807,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return await this.restoreWithServer_sdk(id, userId, asAdmin);
|
||||
return await this.restoreWithServerUsingSdk(id, userId, asAdmin);
|
||||
}
|
||||
|
||||
let response;
|
||||
@@ -1943,26 +1820,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.restore({ id: id, revisionDate: response.revisionDate }, userId);
|
||||
}
|
||||
|
||||
private async restoreWithServer_sdk(id: string, userId: UserId, asAdmin = false): Promise<any> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
if (asAdmin) {
|
||||
await ref.value.vault().ciphers().admin().restore(asUuid(id));
|
||||
} else {
|
||||
await ref.value.vault().ciphers().restore(asUuid(id));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to restore cipher: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
private async restoreWithServerUsingSdk(
|
||||
id: string,
|
||||
userId: UserId,
|
||||
asAdmin = false,
|
||||
): Promise<any> {
|
||||
await this.cipherSdkService.restoreWithServer(id, userId, asAdmin);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
@@ -1975,7 +1838,7 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
if (useSdk) {
|
||||
return await this.restoreManyWithServer_sdk(ids, userId, orgId);
|
||||
return await this.restoreManyWithServerUsingSdk(ids, userId, orgId);
|
||||
}
|
||||
|
||||
let response;
|
||||
@@ -1995,43 +1858,12 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
await this.restore(restores, userId);
|
||||
}
|
||||
|
||||
private async restoreManyWithServer_sdk(
|
||||
private async restoreManyWithServerUsingSdk(
|
||||
ids: string[],
|
||||
userId: UserId,
|
||||
orgId?: string,
|
||||
): Promise<void> {
|
||||
await firstValueFrom(
|
||||
this.sdkService.userClient$(userId).pipe(
|
||||
switchMap(async (sdk) => {
|
||||
if (!sdk) {
|
||||
throw new Error("SDK not available");
|
||||
}
|
||||
using ref = sdk.take();
|
||||
|
||||
// No longer using an asAdmin Param. Org Vault bulkRestore will assess if an item is unassigned or editable
|
||||
// The Org Vault will pass those ids an array as well as the orgId when calling bulkRestore
|
||||
if (orgId) {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.admin()
|
||||
.restore_many(
|
||||
ids.map((id) => asUuid(id)),
|
||||
asUuid(orgId),
|
||||
);
|
||||
} else {
|
||||
await ref.value
|
||||
.vault()
|
||||
.ciphers()
|
||||
.restore_many(ids.map((id) => asUuid(id)));
|
||||
}
|
||||
}),
|
||||
catchError((error: unknown) => {
|
||||
this.logService.error(`Failed to restore multiple ciphers: ${error}`);
|
||||
return EMPTY;
|
||||
}),
|
||||
),
|
||||
);
|
||||
await this.cipherSdkService.restoreManyWithServer(ids, userId, orgId);
|
||||
await this.clearCache(userId);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user