diff --git a/libs/common/src/enums/feature-flag.enum.ts b/libs/common/src/enums/feature-flag.enum.ts index ea52e50b2ed..2f61b4006cc 100644 --- a/libs/common/src/enums/feature-flag.enum.ts +++ b/libs/common/src/enums/feature-flag.enum.ts @@ -61,6 +61,7 @@ export enum FeatureFlag { /* Vault */ PM19941MigrateCipherDomainToSdk = "pm-19941-migrate-cipher-domain-to-sdk", + PM28190CipherSharingOpsToSdk = "pm-28190-cipher-sharing-ops-to-sdk", PM22134SdkCipherListView = "pm-22134-sdk-cipher-list-view", PM22136_SdkCipherEncryption = "pm-22136-sdk-cipher-encryption", CipherKeyEncryption = "cipher-key-encryption", @@ -69,6 +70,7 @@ export enum FeatureFlag { BrowserPremiumSpotlight = "pm-23384-browser-premium-spotlight", MigrateMyVaultToMyItems = "pm-20558-migrate-myvault-to-myitems", PM27632_SdkCipherCrudOperations = "pm-27632-cipher-crud-operations-to-sdk", + PM28190_SdkCipherShareOperations = "pm-28190-cipher-sharing-ops-to-sdk", /* Platform */ IpcChannelFramework = "ipc-channel-framework", @@ -132,6 +134,7 @@ export const DefaultFeatureFlagValue = { [FeatureFlag.VaultLoadingSkeletons]: FALSE, [FeatureFlag.BrowserPremiumSpotlight]: FALSE, [FeatureFlag.PM27632_SdkCipherCrudOperations]: FALSE, + [FeatureFlag.PM28190_SdkCipherShareOperations]: FALSE, [FeatureFlag.MigrateMyVaultToMyItems]: FALSE, /* Auth */ diff --git a/libs/common/src/vault/abstractions/cipher-sdk.service.ts b/libs/common/src/vault/abstractions/cipher-sdk.service.ts index 805a30221ca..b8b7f5090da 100644 --- a/libs/common/src/vault/abstractions/cipher-sdk.service.ts +++ b/libs/common/src/vault/abstractions/cipher-sdk.service.ts @@ -1,4 +1,5 @@ -import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { CollectionId, OrganizationId, UserId } from "@bitwarden/common/types/guid"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; /** @@ -138,4 +139,40 @@ export abstract class CipherSdkService { userId: UserId, includeMemberItems: boolean, ): Promise; + + /** + * Shares a cipher with an organization using the SDK. + * Handles encryption and API call in one operation. + * + * @param cipherView The cipher view to share + * @param organizationId The organization to share with + * @param collectionIds The collection IDs to add the cipher to + * @param userId The user ID to use for SDK client + * @param originalCipherView Optional original cipher view for password history tracking + * @returns A promise that resolves to the shared cipher (encrypted) + */ + abstract shareWithServer( + cipherView: CipherView, + organizationId: OrganizationId, + collectionIds: CollectionId[], + userId: UserId, + originalCipherView?: CipherView, + ): Promise; + + /** + * Shares multiple ciphers with an organization using the SDK. + * Handles encryption and API calls in one operation. + * + * @param cipherViews The cipher views to share + * @param organizationId The organization to share with + * @param collectionIds The collection IDs to add the ciphers to + * @param userId The user ID to use for SDK client + * @returns A promise that resolves to the shared ciphers (encrypted) + */ + abstract shareManyWithServer( + cipherViews: CipherView[], + organizationId: OrganizationId, + collectionIds: CollectionId[], + userId: UserId, + ): Promise; } diff --git a/libs/common/src/vault/abstractions/cipher.service.ts b/libs/common/src/vault/abstractions/cipher.service.ts index 3e8166ba312..3592cdef8d0 100644 --- a/libs/common/src/vault/abstractions/cipher.service.ts +++ b/libs/common/src/vault/abstractions/cipher.service.ts @@ -145,14 +145,14 @@ export abstract class CipherService implements UserKeyRotationDataProvider; abstract shareManyWithServer( ciphers: CipherView[], diff --git a/libs/common/src/vault/services/cipher-sdk.service.spec.ts b/libs/common/src/vault/services/cipher-sdk.service.spec.ts index 7640627b774..110ec7f7db0 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.spec.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.spec.ts @@ -3,7 +3,8 @@ 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 { UserId, CipherId, OrganizationId, CollectionId } from "@bitwarden/common/types/guid"; +import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; import { CipherType } from "../enums/cipher-type"; @@ -46,6 +47,8 @@ describe("DefaultCipherSdkService", () => { restore: jest.fn().mockResolvedValue(undefined), restore_many: jest.fn().mockResolvedValue(undefined), list: jest.fn().mockResolvedValue({ successes: [], failures: [] }), + share_cipher: jest.fn(), + share_ciphers_bulk: jest.fn(), admin: jest.fn().mockReturnValue(mockAdminSdk), }; mockVaultSdk = { @@ -664,4 +667,225 @@ describe("DefaultCipherSdkService", () => { ); }); }); + + describe("shareWithServer()", () => { + const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId; + const collectionId2 = "7ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b24" as CollectionId; + + const createMockSdkCipher = (id: string): any => ({ + id: id, + organizationId: orgId, + folderId: null, + collectionIds: [], + key: null, + name: "EncryptedString", + notes: null, + type: CipherType.Login, + login: null, + identity: null, + card: null, + secureNote: null, + sshKey: null, + data: null, + favorite: false, + reprompt: 0, + organizationUseTotp: false, + edit: true, + permissions: null, + viewPassword: true, + localData: null, + attachments: null, + fields: null, + passwordHistory: null, + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + archivedDate: null, + revisionDate: "2022-01-31T12:00:00.000Z", + }); + + it("should share cipher using SDK", async () => { + const cipherView = new CipherView(); + cipherView.id = cipherId; + cipherView.type = CipherType.Login; + cipherView.name = "Test Cipher"; + + const mockSdkCipher = createMockSdkCipher(cipherId); + mockCiphersSdk.share_cipher.mockResolvedValue(mockSdkCipher); + + const result = await cipherSdkService.shareWithServer( + cipherView, + orgId, + [collectionId1, collectionId2], + userId, + ); + + expect(sdkService.userClient$).toHaveBeenCalledWith(userId); + expect(mockVaultSdk.ciphers).toHaveBeenCalled(); + expect(mockCiphersSdk.share_cipher).toHaveBeenCalledWith( + expect.objectContaining({ + name: cipherView.name, + }), + orgId, + [collectionId1, collectionId2], + null, + ); + expect(result).toBeInstanceOf(Cipher); + }); + + it("should pass null for original cipher when originalCipherView is provided", async () => { + const cipherView = new CipherView(); + cipherView.id = cipherId; + cipherView.type = CipherType.Login; + cipherView.name = "Test Cipher"; + + const originalCipherView = new CipherView(); + originalCipherView.id = cipherId; + originalCipherView.name = "Original Cipher"; + + const mockSdkCipher = createMockSdkCipher(cipherId); + mockCiphersSdk.share_cipher.mockResolvedValue(mockSdkCipher); + + await cipherSdkService.shareWithServer( + cipherView, + orgId, + [collectionId1], + userId, + originalCipherView, + ); + + // SDK handles history internally, so we always pass null for original_cipher + expect(mockCiphersSdk.share_cipher).toHaveBeenCalledWith( + expect.anything(), + orgId, + [collectionId1], + null, + ); + }); + + 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.shareWithServer(cipherView, orgId, [collectionId1], userId), + ).rejects.toThrow("SDK not available"); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to share cipher"), + ); + }); + + it("should throw error and log when SDK throws an error", async () => { + const cipherView = new CipherView(); + cipherView.name = "Test Cipher"; + + mockCiphersSdk.share_cipher.mockRejectedValue(new Error("SDK error")); + + await expect( + cipherSdkService.shareWithServer(cipherView, orgId, [collectionId1], userId), + ).rejects.toThrow(); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to share cipher"), + ); + }); + }); + + describe("shareManyWithServer()", () => { + const collectionId1 = "6ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b23" as CollectionId; + const cipherId2 = "8ff8c0b2-1d3e-4f8c-9b2d-1d3e4f8c0b25" as CipherId; + + const createMockSdkCipher = (id: string): any => ({ + id: id, + organizationId: orgId, + folderId: null, + collectionIds: [], + key: null, + name: "EncryptedString", + notes: null, + type: CipherType.Login, + login: null, + identity: null, + card: null, + secureNote: null, + sshKey: null, + data: null, + favorite: false, + reprompt: 0, + organizationUseTotp: false, + edit: true, + permissions: null, + viewPassword: true, + localData: null, + attachments: null, + fields: null, + passwordHistory: null, + creationDate: "2022-01-01T12:00:00.000Z", + deletedDate: null, + archivedDate: null, + revisionDate: "2022-01-31T12:00:00.000Z", + }); + + it("should share multiple ciphers using SDK", async () => { + const cipherView1 = new CipherView(); + cipherView1.id = cipherId; + cipherView1.type = CipherType.Login; + cipherView1.name = "Test Cipher 1"; + + const cipherView2 = new CipherView(); + cipherView2.id = cipherId2; + cipherView2.type = CipherType.Login; + cipherView2.name = "Test Cipher 2"; + + const mockSdkCiphers = [createMockSdkCipher(cipherId), createMockSdkCipher(cipherId2)]; + mockCiphersSdk.share_ciphers_bulk.mockResolvedValue(mockSdkCiphers); + + const result = await cipherSdkService.shareManyWithServer( + [cipherView1, cipherView2], + orgId, + [collectionId1], + userId, + ); + + expect(sdkService.userClient$).toHaveBeenCalledWith(userId); + expect(mockVaultSdk.ciphers).toHaveBeenCalled(); + expect(mockCiphersSdk.share_ciphers_bulk).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ name: cipherView1.name }), + expect.objectContaining({ name: cipherView2.name }), + ]), + orgId, + [collectionId1], + ); + expect(result).toHaveLength(2); + expect(result[0]).toBeInstanceOf(Cipher); + expect(result[1]).toBeInstanceOf(Cipher); + }); + + 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.shareManyWithServer([cipherView], orgId, [collectionId1], userId), + ).rejects.toThrow("SDK not available"); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to share multiple ciphers"), + ); + }); + + it("should throw error and log when SDK throws an error", async () => { + const cipherView = new CipherView(); + cipherView.name = "Test Cipher"; + + mockCiphersSdk.share_ciphers_bulk.mockRejectedValue(new Error("SDK error")); + + await expect( + cipherSdkService.shareManyWithServer([cipherView], orgId, [collectionId1], userId), + ).rejects.toThrow(); + expect(logService.error).toHaveBeenCalledWith( + expect.stringContaining("Failed to share multiple ciphers"), + ); + }); + }); }); diff --git a/libs/common/src/vault/services/cipher-sdk.service.ts b/libs/common/src/vault/services/cipher-sdk.service.ts index 8e60fb6e2c7..2ad3e903cb3 100644 --- a/libs/common/src/vault/services/cipher-sdk.service.ts +++ b/libs/common/src/vault/services/cipher-sdk.service.ts @@ -3,7 +3,7 @@ import { firstValueFrom, switchMap, catchError } from "rxjs"; import { DECRYPT_ERROR } from "@bitwarden/common/key-management/crypto/models/enc-string"; 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 { CollectionId, 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"; @@ -336,4 +336,77 @@ export class DefaultCipherSdkService implements CipherSdkService { ), ); } + + async shareWithServer( + cipherView: CipherView, + organizationId: OrganizationId, + collectionIds: CollectionId[], + userId: UserId, + _originalCipherView?: CipherView, + ): Promise { + return await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + switchMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + + const sdkCipherView = cipherView.toSdkCipherView(); + + // SDK handles cipher history adjustment internally, so we pass null for original_cipher + const result = await ref.value + .vault() + .ciphers() + .share_cipher( + sdkCipherView, + asUuid(organizationId), + collectionIds.map((id) => asUuid(id)), + null, + ); + + return Cipher.fromSdkCipher(result); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to share cipher: ${error}`); + throw error; + }), + ), + ); + } + + async shareManyWithServer( + cipherViews: CipherView[], + organizationId: OrganizationId, + collectionIds: CollectionId[], + userId: UserId, + ): Promise { + return await firstValueFrom( + this.sdkService.userClient$(userId).pipe( + switchMap(async (sdk) => { + if (!sdk) { + throw new Error("SDK not available"); + } + using ref = sdk.take(); + + const sdkCipherViews = cipherViews.map((cv) => cv.toSdkCipherView()); + + const results = await ref.value + .vault() + .ciphers() + .share_ciphers_bulk( + sdkCipherViews, + asUuid(organizationId), + collectionIds.map((id) => asUuid(id)), + ); + + return results.map((c) => Cipher.fromSdkCipher(c)); + }), + catchError((error: unknown) => { + this.logService.error(`Failed to share multiple ciphers: ${error}`); + throw error; + }), + ), + ); + } } diff --git a/libs/common/src/vault/services/cipher.service.spec.ts b/libs/common/src/vault/services/cipher.service.spec.ts index 0a83f7101aa..dcafc3efbd5 100644 --- a/libs/common/src/vault/services/cipher.service.spec.ts +++ b/libs/common/src/vault/services/cipher.service.spec.ts @@ -117,8 +117,9 @@ describe("Cipher Service", () => { let cipherService: CipherService; let encryptionContext: EncryptionContext; - // BehaviorSubject for SDK feature flag - allows tests to change the value after service instantiation + // BehaviorSubjects for SDK feature flags - allows tests to change the value after service instantiation let sdkCrudFeatureFlag$: BehaviorSubject; + let sdkShareFeatureFlag$: BehaviorSubject; beforeEach(() => { encryptService.encryptFileData.mockReturnValue(Promise.resolve(ENCRYPTED_BYTES)); @@ -134,9 +135,15 @@ describe("Cipher Service", () => { (window as any).bitwardenContainerService = new ContainerService(keyService, encryptService); - // Create BehaviorSubject for SDK feature flag - tests can update this to change behavior + // Create BehaviorSubjects for SDK feature flags - tests can update these to change behavior sdkCrudFeatureFlag$ = new BehaviorSubject(false); - configService.getFeatureFlag$.mockReturnValue(sdkCrudFeatureFlag$.asObservable()); + sdkShareFeatureFlag$ = new BehaviorSubject(false); + configService.getFeatureFlag$.mockImplementation((flag: FeatureFlag) => { + if (flag === FeatureFlag.PM28190_SdkCipherShareOperations) { + return sdkShareFeatureFlag$.asObservable(); + } + return sdkCrudFeatureFlag$.asObservable(); + }); cipherService = new CipherService( keyService, @@ -889,6 +896,124 @@ describe("Cipher Service", () => { userId, ); }); + + it("should delegate to cipherSdkService when SDK share feature flag is enabled", async () => { + sdkShareFeatureFlag$.next(true); + + const expectedCipher = new Cipher(cipherData); + expectedCipher.organizationId = orgId; + const cipherView = new CipherView(expectedCipher); + cipherView.organizationId = null; // Ensure organizationId is null for this test + const collectionIds = ["collection1", "collection2"] as CollectionId[]; + + const sdkServiceSpy = jest + .spyOn(cipherSdkService, "shareWithServer") + .mockResolvedValue(expectedCipher); + const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache"); + + const result = await cipherService.shareWithServer(cipherView, orgId, collectionIds, userId); + + expect(sdkServiceSpy).toHaveBeenCalledWith( + cipherView, + orgId, + collectionIds, + userId, + undefined, + ); + expect(clearCacheSpy).toHaveBeenCalledWith(userId); + expect(result).toEqual(expectedCipher); + }); + + it("should pass originalCipherView to cipherSdkService when SDK share feature flag is enabled", async () => { + sdkShareFeatureFlag$.next(true); + + const expectedCipher = new Cipher(cipherData); + const cipherView = new CipherView(expectedCipher); + cipherView.organizationId = null; + const originalCipherView = new CipherView(expectedCipher); + originalCipherView.name = "Original Cipher"; + const collectionIds = ["collection1"] as CollectionId[]; + + const sdkServiceSpy = jest + .spyOn(cipherSdkService, "shareWithServer") + .mockResolvedValue(expectedCipher); + + await cipherService.shareWithServer( + cipherView, + orgId, + collectionIds, + userId, + originalCipherView, + ); + + expect(sdkServiceSpy).toHaveBeenCalledWith( + cipherView, + orgId, + collectionIds, + userId, + originalCipherView, + ); + }); + + it("should throw when cipher already has organization and SDK share flag is enabled", async () => { + sdkShareFeatureFlag$.next(true); + + const expectedCipher = new Cipher(cipherData); + expectedCipher.organizationId = orgId; + const cipherView = new CipherView(expectedCipher); + cipherView.organizationId = orgId; // Cipher already has organization + const collectionIds = ["collection1"] as CollectionId[]; + + await expect( + cipherService.shareWithServer(cipherView, orgId, collectionIds, userId), + ).rejects.toThrow("Cipher is already associated with an organization."); + }); + }); + + describe("shareManyWithServer()", () => { + it("should delegate to cipherSdkService when SDK share feature flag is enabled", async () => { + sdkShareFeatureFlag$.next(true); + + const cipherView1 = new CipherView(new Cipher(cipherData)); + cipherView1.organizationId = null; + const cipherView2 = new CipherView(new Cipher(cipherData)); + cipherView2.organizationId = null; + const collectionIds = ["collection1"] as CollectionId[]; + + const sdkServiceSpy = jest + .spyOn(cipherSdkService, "shareManyWithServer") + .mockResolvedValue([]); + const clearCacheSpy = jest.spyOn(cipherService as any, "clearCache"); + + await cipherService.shareManyWithServer( + [cipherView1, cipherView2], + orgId, + collectionIds, + userId, + ); + + expect(sdkServiceSpy).toHaveBeenCalledWith( + [cipherView1, cipherView2], + orgId, + collectionIds, + userId, + ); + expect(clearCacheSpy).toHaveBeenCalledWith(userId); + }); + + it("should throw when any cipher already has organization and SDK share flag is enabled", async () => { + sdkShareFeatureFlag$.next(true); + + const cipherView1 = new CipherView(new Cipher(cipherData)); + cipherView1.organizationId = null; + const cipherView2 = new CipherView(new Cipher(cipherData)); + cipherView2.organizationId = orgId; // Second cipher already has organization + const collectionIds = ["collection1"] as CollectionId[]; + + await expect( + cipherService.shareManyWithServer([cipherView1, cipherView2], orgId, collectionIds, userId), + ).rejects.toThrow("Cipher is already associated with an organization."); + }); }); describe("decryptCiphers", () => { diff --git a/libs/common/src/vault/services/cipher.service.ts b/libs/common/src/vault/services/cipher.service.ts index b132dfb23ec..db484af8e36 100644 --- a/libs/common/src/vault/services/cipher.service.ts +++ b/libs/common/src/vault/services/cipher.service.ts @@ -113,6 +113,10 @@ export class CipherService implements CipherServiceAbstraction { FeatureFlag.PM27632_SdkCipherCrudOperations, ); + private readonly sdkCipherShareEnabled$: Observable = this.configService.getFeatureFlag$( + FeatureFlag.PM28190_SdkCipherShareOperations, + ); + constructor( private keyService: KeyService, private domainSettingsService: DomainSettingsService, @@ -1086,12 +1090,31 @@ export class CipherService implements CipherServiceAbstraction { organizationId: string, collectionIds: string[], userId: UserId, - originalCipher?: Cipher, + originalCipherView?: CipherView, ): Promise { + const useSdkShare = await firstValueFrom(this.sdkCipherShareEnabled$); + if (useSdkShare) { + return this.shareWithServerUsingSdk( + cipher, + organizationId, + collectionIds, + userId, + originalCipherView, + ); + } + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( FeatureFlag.PM22136_SdkCipherEncryption, ); + // Get original cipher for adjustCipherHistory + let originalCipher: Cipher | undefined; + if (originalCipherView) { + // Encrypt the provided originalCipherView + const encryptResult = await this.cipherEncryptionService.encrypt(originalCipherView, userId); + originalCipher = encryptResult?.cipher; + } + // If originalCipher is undefined, adjustCipherHistory will fetch from cache await this.adjustCipherHistory(cipher, userId, originalCipher); let encCipher: EncryptionContext; @@ -1146,6 +1169,11 @@ export class CipherService implements CipherServiceAbstraction { collectionIds: string[], userId: UserId, ) { + const useSdkShare = await firstValueFrom(this.sdkCipherShareEnabled$); + if (useSdkShare) { + return this.shareManyWithServerUsingSdk(ciphers, organizationId, collectionIds, userId); + } + const sdkCipherEncryptionEnabled = await this.configService.getFeatureFlag( FeatureFlag.PM22136_SdkCipherEncryption, ); @@ -1199,6 +1227,51 @@ export class CipherService implements CipherServiceAbstraction { } } + private async shareWithServerUsingSdk( + cipher: CipherView, + organizationId: string, + collectionIds: string[], + userId: UserId, + originalCipherView?: CipherView, + ): Promise { + if (cipher.organizationId != null) { + throw new Error("Cipher is already associated with an organization."); + } + + const result = await this.cipherSdkService.shareWithServer( + cipher, + organizationId as OrganizationId, + collectionIds as CollectionId[], + userId, + originalCipherView, + ); + + await this.clearCache(userId); + return result; + } + + private async shareManyWithServerUsingSdk( + ciphers: CipherView[], + organizationId: string, + collectionIds: string[], + userId: UserId, + ): Promise { + for (const cipher of ciphers) { + if (cipher.organizationId != null) { + throw new Error("Cipher is already associated with an organization."); + } + } + + await this.cipherSdkService.shareManyWithServer( + ciphers, + organizationId as OrganizationId, + collectionIds as CollectionId[], + userId, + ); + + await this.clearCache(userId); + } + saveAttachmentWithServer( cipher: Cipher, unencryptedFile: any, diff --git a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts index 8566e51d74f..39287def687 100644 --- a/libs/vault/src/cipher-form/services/default-cipher-form.service.ts +++ b/libs/vault/src/cipher-form/services/default-cipher-form.service.ts @@ -61,7 +61,7 @@ export class DefaultCipherFormService implements CipherFormService { organizationId, cipher.collectionIds, activeUserId, - config.originalCipher, + originalCipherView, ); // If the collectionIds are the same, update the cipher normally } else if (isSetEqual(originalCollectionIds, newCollectionIds)) {