mirror of
https://github.com/bitwarden/browser
synced 2026-01-31 00:33:33 +00:00
Initial impl of sharing operations with SDK
This commit is contained in:
@@ -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 */
|
||||
|
||||
@@ -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<CipherView[]>;
|
||||
|
||||
/**
|
||||
* 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<Cipher>;
|
||||
|
||||
/**
|
||||
* 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<Cipher[]>;
|
||||
}
|
||||
|
||||
@@ -145,14 +145,14 @@ export abstract class CipherService implements UserKeyRotationDataProvider<Ciphe
|
||||
* @param organizationId The Id of the organization to move the cipher to
|
||||
* @param collectionIds The collection Ids to assign the cipher to in the organization
|
||||
* @param userId The Id of the user performing the operation
|
||||
* @param originalCipher Optional original cipher that will be used to compare/update password history
|
||||
* @param originalCipherView Optional original cipher view that will be used to compare/update password history
|
||||
*/
|
||||
abstract shareWithServer(
|
||||
cipher: CipherView,
|
||||
organizationId: string,
|
||||
collectionIds: string[],
|
||||
userId: UserId,
|
||||
originalCipher?: Cipher,
|
||||
originalCipherView?: CipherView,
|
||||
): Promise<Cipher>;
|
||||
abstract shareManyWithServer(
|
||||
ciphers: CipherView[],
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<Cipher> {
|
||||
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<Cipher[]> {
|
||||
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;
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<boolean>;
|
||||
let sdkShareFeatureFlag$: BehaviorSubject<boolean>;
|
||||
|
||||
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<boolean>(false);
|
||||
configService.getFeatureFlag$.mockReturnValue(sdkCrudFeatureFlag$.asObservable());
|
||||
sdkShareFeatureFlag$ = new BehaviorSubject<boolean>(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", () => {
|
||||
|
||||
@@ -113,6 +113,10 @@ export class CipherService implements CipherServiceAbstraction {
|
||||
FeatureFlag.PM27632_SdkCipherCrudOperations,
|
||||
);
|
||||
|
||||
private readonly sdkCipherShareEnabled$: Observable<boolean> = 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<Cipher> {
|
||||
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<Cipher> {
|
||||
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<void> {
|
||||
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,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
Reference in New Issue
Block a user