diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index 15e7d3cc34..96178cc6dd 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -1097,7 +1097,6 @@ export default class MainBackground { this.encryptService, this.cryptoFunctionService, this.kdfConfigService, - this.accountService, this.apiService, this.restrictedItemTypesService, ); @@ -1113,13 +1112,13 @@ export default class MainBackground { this.cryptoFunctionService, this.collectionService, this.kdfConfigService, - this.accountService, this.restrictedItemTypesService, ); this.exportService = new VaultExportService( this.individualVaultExportService, this.organizationVaultExportService, + this.accountService, ); this.browserInitialInstallService = new BrowserInitialInstallService(this.stateProvider); diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index 4413df4abc..bcfb53dc01 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -841,7 +841,6 @@ export class ServiceContainer { this.encryptService, this.cryptoFunctionService, this.kdfConfigService, - this.accountService, this.apiService, this.restrictedItemTypesService, ); @@ -857,13 +856,13 @@ export class ServiceContainer { this.cryptoFunctionService, this.collectionService, this.kdfConfigService, - this.accountService, this.restrictedItemTypesService, ); this.exportService = new VaultExportService( this.individualExportService, this.organizationExportService, + this.accountService, ); this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService); diff --git a/apps/cli/src/tools/export.command.ts b/apps/cli/src/tools/export.command.ts index 3fbc466efe..fd35b845be 100644 --- a/apps/cli/src/tools/export.command.ts +++ b/apps/cli/src/tools/export.command.ts @@ -70,10 +70,13 @@ export class ExportCommand { password = await this.promptPassword(password); } + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + exportContent = options.organizationid == null - ? await this.exportService.getExport(format, password) + ? await this.exportService.getExport(userId, format, password) : await this.exportService.getOrganizationExport( + userId, options.organizationid, format, password, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index b51e8847ff..213a0c2618 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -884,7 +884,6 @@ const safeProviders: SafeProvider[] = [ EncryptService, CryptoFunctionServiceAbstraction, KdfConfigService, - AccountServiceAbstraction, ApiServiceAbstraction, RestrictedItemTypesService, ], @@ -906,14 +905,17 @@ const safeProviders: SafeProvider[] = [ CryptoFunctionServiceAbstraction, CollectionService, KdfConfigService, - AccountServiceAbstraction, RestrictedItemTypesService, ], }), safeProvider({ provide: VaultExportServiceAbstraction, useClass: VaultExportService, - deps: [IndividualVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction], + deps: [ + IndividualVaultExportServiceAbstraction, + OrganizationVaultExportServiceAbstraction, + AccountServiceAbstraction, + ], }), safeProvider({ provide: SearchServiceAbstraction, diff --git a/libs/tools/export/vault-export/vault-export-core/jest.config.js b/libs/tools/export/vault-export/vault-export-core/jest.config.js index 066309a8bf..68c286de3d 100644 --- a/libs/tools/export/vault-export/vault-export-core/jest.config.js +++ b/libs/tools/export/vault-export/vault-export-core/jest.config.js @@ -7,7 +7,10 @@ module.exports = { testMatch: ["**/+(*.)+(spec).+(ts)"], preset: "ts-jest", testEnvironment: "jsdom", - moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { - prefix: "/../../../../../", - }), + moduleNameMapper: pathsToModuleNameMapper( + { "@bitwarden/common/spec": ["libs/common/spec"], ...(compilerOptions?.paths ?? {}) }, + { + prefix: "/../../../../../", + }, + ), }; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts index a2adae8aa9..e34094c6ef 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.abstraction.ts @@ -1,8 +1,10 @@ +import { UserId } from "@bitwarden/common/types/guid"; + import { ExportedVault } from "../types"; import { ExportFormat } from "./vault-export.service.abstraction"; export abstract class IndividualVaultExportServiceAbstraction { - abstract getExport: (format: ExportFormat) => Promise; - abstract getPasswordProtectedExport: (password: string) => Promise; + abstract getExport: (userId: UserId, format: ExportFormat) => Promise; + abstract getPasswordProtectedExport: (userId: UserId, password: string) => Promise; } diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts index 7552f675c6..a2df4ec27d 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.spec.ts @@ -3,7 +3,6 @@ import * as JSZip from "jszip"; import { BehaviorSubject, of } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { @@ -175,7 +174,6 @@ describe("VaultExportService", () => { let keyService: MockProxy; let encryptService: MockProxy; let kdfConfigService: MockProxy; - let accountService: MockProxy; let apiService: MockProxy; let restrictedSubject: BehaviorSubject; let restrictedItemTypesService: Partial; @@ -191,7 +189,6 @@ describe("VaultExportService", () => { keyService = mock(); encryptService = mock(); kdfConfigService = mock(); - accountService = mock(); apiService = mock(); keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); @@ -202,14 +199,6 @@ describe("VaultExportService", () => { isCipherRestricted$: jest.fn().mockReturnValue(of(false)), }; - const accountInfo: AccountInfo = { - email: "", - emailVerified: true, - name: undefined, - }; - const activeAccount = { id: userId, ...accountInfo }; - accountService.activeAccount$ = new BehaviorSubject(activeAccount); - fetchMock = jest.fn().mockResolvedValue({}); global.fetch = fetchMock; @@ -236,7 +225,6 @@ describe("VaultExportService", () => { encryptService, cryptoFunctionService, kdfConfigService, - accountService, apiService, restrictedItemTypesService as RestrictedItemTypesService, ); @@ -245,7 +233,7 @@ describe("VaultExportService", () => { it("exports unencrypted user ciphers", async () => { cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); - const actual = await exportService.getExport("json"); + const actual = await exportService.getExport(userId, "json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; expectEqualCiphers(UserCipherViews.slice(0, 1), exportedData.data); @@ -254,7 +242,7 @@ describe("VaultExportService", () => { it("exports encrypted json user ciphers", async () => { cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); - const actual = await exportService.getExport("encrypted_json"); + const actual = await exportService.getExport(userId, "encrypted_json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; expectEqualCiphers(UserCipherDomains.slice(0, 1), exportedData.data); @@ -263,7 +251,7 @@ describe("VaultExportService", () => { it("does not unencrypted export trashed user items", async () => { cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews); - const actual = await exportService.getExport("json"); + const actual = await exportService.getExport(userId, "json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; expectEqualCiphers(UserCipherViews.slice(0, 2), exportedData.data); @@ -272,7 +260,7 @@ describe("VaultExportService", () => { it("does not encrypted export trashed user items", async () => { cipherService.getAll.mockResolvedValue(UserCipherDomains); - const actual = await exportService.getExport("encrypted_json"); + const actual = await exportService.getExport(userId, "encrypted_json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data); @@ -291,7 +279,7 @@ describe("VaultExportService", () => { const testCiphers = [UserCipherViews[0], cardCipher, UserCipherViews[1]]; cipherService.getAllDecrypted.mockResolvedValue(testCiphers); - const actual = await exportService.getExport("json"); + const actual = await exportService.getExport(userId, "json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; @@ -311,7 +299,7 @@ describe("VaultExportService", () => { const testCiphers = [UserCipherDomains[0], cardCipher, UserCipherDomains[1]]; cipherService.getAll.mockResolvedValue(testCiphers); - const actual = await exportService.getExport("encrypted_json"); + const actual = await exportService.getExport(userId, "encrypted_json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; @@ -323,7 +311,7 @@ describe("VaultExportService", () => { cipherService.getAllDecrypted.mockResolvedValue([]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - const exportedVault = await exportService.getExport("zip"); + const exportedVault = await exportService.getExport(userId, "zip"); expect(exportedVault.type).toBe("application/zip"); const exportZip = exportedVault as ExportedVaultAsBlob; @@ -348,7 +336,7 @@ describe("VaultExportService", () => { cipherService.getAllDecrypted.mockResolvedValue([cipherView, orgCipherView]); folderService.getAllDecryptedFromState.mockResolvedValue([]); - const exportedVault = await exportService.getExport("zip"); + const exportedVault = await exportService.getExport(userId, "zip"); const zip = await JSZip.loadAsync(exportedVault.data); const data = await zip.file("data.json")?.async("string"); @@ -380,7 +368,7 @@ describe("VaultExportService", () => { global.Request = jest.fn(() => {}) as any; await expect(async () => { - await exportService.getExport("zip"); + await exportService.getExport(userId, "zip"); }).rejects.toThrow("Error downloading attachment"); }, ); @@ -408,7 +396,7 @@ describe("VaultExportService", () => { global.Request = jest.fn(() => {}) as any; await expect(async () => { - await exportService.getExport("zip"); + await exportService.getExport(userId, "zip"); }).rejects.toThrow("Error decrypting attachment"); }); @@ -434,7 +422,7 @@ describe("VaultExportService", () => { ) as any; global.Request = jest.fn(() => {}) as any; - const exportedVault = await exportService.getExport("zip"); + const exportedVault = await exportService.getExport(userId, "zip"); expect(exportedVault.type).toBe("application/zip"); const exportZip = exportedVault as ExportedVaultAsBlob; @@ -464,7 +452,7 @@ describe("VaultExportService", () => { jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); - exportedVault = await exportService.getPasswordProtectedExport(password); + exportedVault = await exportService.getPasswordProtectedExport(userId, password); exportString = exportedVault.data; exportObject = JSON.parse(exportString); }); @@ -491,7 +479,7 @@ describe("VaultExportService", () => { it("has a mac property", async () => { encryptService.encryptString.mockResolvedValue(mac); - exportedVault = await exportService.getPasswordProtectedExport(password); + exportedVault = await exportService.getPasswordProtectedExport(userId, password); exportString = exportedVault.data; exportObject = JSON.parse(exportString); @@ -500,7 +488,7 @@ describe("VaultExportService", () => { it("has data property", async () => { encryptService.encryptString.mockResolvedValue(data); - exportedVault = await exportService.getPasswordProtectedExport(password); + exportedVault = await exportService.getPasswordProtectedExport(userId, password); exportString = exportedVault.data; exportObject = JSON.parse(exportString); @@ -508,7 +496,7 @@ describe("VaultExportService", () => { }); it("encrypts the data property", async () => { - const unEncryptedExportVault = await exportService.getExport(); + const unEncryptedExportVault = await exportService.getExport(userId); const unEncryptedExportString = unEncryptedExportVault.data; expect(exportObject.data).not.toEqual(unEncryptedExportString); @@ -520,7 +508,7 @@ describe("VaultExportService", () => { cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); folderService.folderViews$.mockReturnValue(of(UserFolderViews)); - const actual = await exportService.getExport("json"); + const actual = await exportService.getExport(userId, "json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; @@ -531,7 +519,7 @@ describe("VaultExportService", () => { cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); folderService.folders$.mockReturnValue(of(UserFolders)); - const actual = await exportService.getExport("encrypted_json"); + const actual = await exportService.getExport(userId, "encrypted_json"); expect(typeof actual.data).toBe("string"); const exportedData = actual as ExportedVaultAsString; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts index 2a6a97f5d4..e51c9543bb 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/individual-vault-export.service.ts @@ -5,8 +5,6 @@ import * as papa from "papaparse"; import { firstValueFrom } from "rxjs"; import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; @@ -49,7 +47,6 @@ export class IndividualVaultExportService encryptService: EncryptService, cryptoFunctionService: CryptoFunctionService, kdfConfigService: KdfConfigService, - private accountService: AccountService, private apiService: ApiService, private restrictedItemTypesService: RestrictedItemTypesService, ) { @@ -57,10 +54,10 @@ export class IndividualVaultExportService } /** Creates an export of an individual vault (My Vault). Based on the provided format it will either be unencrypted, encrypted or password protected and in case zip is selected will include attachments + * @param userId The userId of the account requesting the export * @param format The format of the export */ - async getExport(format: ExportFormat = "csv"): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + async getExport(userId: UserId, format: ExportFormat = "csv"): Promise { if (format === "encrypted_json") { return this.getEncryptedExport(userId); } else if (format === "zip") { @@ -70,12 +67,15 @@ export class IndividualVaultExportService } /** Creates a password protected export of an individual vault (My Vault) as a JSON file + * @param userId The userId of the account requesting the export * @param password The password to encrypt the export with * @returns A password-protected encrypted individual vault export */ - async getPasswordProtectedExport(password: string): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); - const exportVault = await this.getExport("json"); + async getPasswordProtectedExport( + userId: UserId, + password: string, + ): Promise { + const exportVault = await this.getExport(userId, "json"); if (exportVault.type !== "text/plain") { throw new Error("Unexpected export type"); diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.abstraction.ts index 3b3b3aaf90..7556b0dfa5 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.abstraction.ts @@ -1,4 +1,4 @@ -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { UserId, OrganizationId } from "@bitwarden/common/types/guid"; import { ExportedVaultAsString } from "../types"; @@ -6,11 +6,13 @@ import { ExportFormat } from "./vault-export.service.abstraction"; export abstract class OrganizationVaultExportServiceAbstraction { abstract getPasswordProtectedExport: ( + userId: UserId, organizationId: OrganizationId, password: string, onlyManagedCollections: boolean, ) => Promise; abstract getOrganizationExport: ( + userId: UserId, organizationId: OrganizationId, format: ExportFormat, onlyManagedCollections: boolean, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts index bf00d1a350..3884dde4b1 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/org-vault-export.service.ts @@ -10,8 +10,6 @@ import { CollectionDetailsResponse, CollectionView, } from "@bitwarden/admin-console/common"; -import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; @@ -52,25 +50,26 @@ export class OrganizationVaultExportService cryptoFunctionService: CryptoFunctionService, private collectionService: CollectionService, kdfConfigService: KdfConfigService, - private accountService: AccountService, private restrictedItemTypesService: RestrictedItemTypesService, ) { super(pinService, encryptService, cryptoFunctionService, kdfConfigService); } /** Creates a password protected export of an organizational vault. + * @param userId The userId of the account requesting the export * @param organizationId The organization id * @param password The password to protect the export * @param onlyManagedCollections If true only managed collections will be exported * @returns The exported vault */ async getPasswordProtectedExport( + userId: UserId, organizationId: OrganizationId, password: string, onlyManagedCollections: boolean, ): Promise { - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); const exportVault = await this.getOrganizationExport( + userId, organizationId, "json", onlyManagedCollections, @@ -84,6 +83,7 @@ export class OrganizationVaultExportService } /** Creates an export of an organizational vault. Based on the provided format it will either be unencrypted, encrypted + * @param userId The userId of the account requesting the export * @param organizationId The organization id * @param format The format of the export * @param onlyManagedCollections If true only managed collections will be exported @@ -94,6 +94,7 @@ export class OrganizationVaultExportService * @throws Error if the organization policies prevent the export */ async getOrganizationExport( + userId: UserId, organizationId: OrganizationId, format: ExportFormat = "csv", onlyManagedCollections: boolean, @@ -105,7 +106,6 @@ export class OrganizationVaultExportService if (format === "zip") { throw new Error("Zip export not supported for organization"); } - const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); if (format === "encrypted_json") { return { diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts index 98eca52b3c..e25fec6eb8 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.abstraction.ts @@ -1,4 +1,4 @@ -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { UserId, OrganizationId } from "@bitwarden/common/types/guid"; import { ExportedVault } from "../types"; @@ -6,8 +6,13 @@ export const EXPORT_FORMATS = ["csv", "json", "encrypted_json", "zip"] as const; export type ExportFormat = (typeof EXPORT_FORMATS)[number]; export abstract class VaultExportServiceAbstraction { - abstract getExport: (format: ExportFormat, password: string) => Promise; + abstract getExport: ( + userId: UserId, + format: ExportFormat, + password: string, + ) => Promise; abstract getOrganizationExport: ( + userId: UserId, organizationId: OrganizationId, format: ExportFormat, password: string, diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts index b86486d45e..0ffe9af52b 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.spec.ts @@ -1,355 +1,153 @@ import { mock, MockProxy } from "jest-mock-extended"; -import { BehaviorSubject, of } from "rxjs"; -import { ApiService } from "@bitwarden/common/abstractions/api.service"; -import { AccountInfo, AccountService } from "@bitwarden/common/auth/abstractions/account.service"; -import { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service"; -import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; -import { - EncryptedString, - EncString, -} from "@bitwarden/common/key-management/crypto/models/enc-string"; -import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; -import { CipherWithIdExport } from "@bitwarden/common/models/export/cipher-with-ids.export"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { UserId } from "@bitwarden/common/types/guid"; -import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; -import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction"; -import { CipherType } from "@bitwarden/common/vault/enums"; -import { Cipher } from "@bitwarden/common/vault/models/domain/cipher"; -import { Folder } from "@bitwarden/common/vault/models/domain/folder"; -import { Login } from "@bitwarden/common/vault/models/domain/login"; -import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view"; -import { FolderView } from "@bitwarden/common/vault/models/view/folder.view"; -import { LoginView } from "@bitwarden/common/vault/models/view/login.view"; -import { - RestrictedCipherType, - RestrictedItemTypesService, -} from "@bitwarden/common/vault/services/restricted-item-types.service"; -import { - DEFAULT_KDF_CONFIG, - PBKDF2KdfConfig, - KdfConfigService, - KeyService, - KdfType, -} from "@bitwarden/key-management"; +import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec"; +import { OrganizationId, UserId } from "@bitwarden/common/types/guid"; -import { BuildTestObject, GetUniqueString } from "../../../../../../common/spec"; -import { ExportedVault, ExportedVaultAsString } from "../types"; - -import { IndividualVaultExportService } from "./individual-vault-export.service"; - -const UserCipherViews = [ - generateCipherView(false), - generateCipherView(false), - generateCipherView(true), -]; - -const UserCipherDomains = [ - generateCipherDomain(false), - generateCipherDomain(false), - generateCipherDomain(true), -]; - -const UserFolderViews = [generateFolderView(), generateFolderView()]; - -const UserFolders = [generateFolder(), generateFolder()]; - -function generateCipherView(deleted: boolean) { - return BuildTestObject( - { - id: GetUniqueString("id"), - notes: GetUniqueString("notes"), - type: CipherType.Login, - login: BuildTestObject( - { - username: GetUniqueString("username"), - password: GetUniqueString("password"), - }, - LoginView, - ), - collectionIds: null, - deletedDate: deleted ? new Date() : null, - }, - CipherView, - ); -} - -function generateCipherDomain(deleted: boolean) { - return BuildTestObject( - { - id: GetUniqueString("id"), - notes: new EncString(GetUniqueString("notes")), - type: CipherType.Login, - login: BuildTestObject( - { - username: new EncString(GetUniqueString("username")), - password: new EncString(GetUniqueString("password")), - }, - Login, - ), - collectionIds: null, - deletedDate: deleted ? new Date() : null, - }, - Cipher, - ); -} - -function generateFolderView() { - return BuildTestObject( - { - id: GetUniqueString("id"), - name: GetUniqueString("name"), - revisionDate: new Date(), - }, - FolderView, - ); -} - -function generateFolder() { - const actual = Folder.fromJSON({ - revisionDate: new Date("2022-08-04T01:06:40.441Z").toISOString(), - name: "name" as EncryptedString, - id: "id", - }); - return actual; -} - -function expectEqualCiphers(ciphers: CipherView[] | Cipher[], jsonResult: string) { - const actual = JSON.stringify(JSON.parse(jsonResult).items); - const items: CipherWithIdExport[] = []; - ciphers.forEach((c: CipherView | Cipher) => { - const item = new CipherWithIdExport(); - item.build(c); - items.push(item); - }); - - expect(actual).toEqual(JSON.stringify(items)); -} - -function expectEqualFolderViews(folderViews: FolderView[] | Folder[], jsonResult: string) { - const actual = JSON.stringify(JSON.parse(jsonResult).folders); - const folders: FolderResponse[] = []; - folderViews.forEach((c) => { - const folder = new FolderResponse(); - folder.id = c.id; - folder.name = c.name.toString(); - folders.push(folder); - }); - - expect(actual.length).toBeGreaterThan(0); - expect(actual).toEqual(JSON.stringify(folders)); -} - -function expectEqualFolders(folders: Folder[], jsonResult: string) { - const actual = JSON.stringify(JSON.parse(jsonResult).folders); - const items: Folder[] = []; - folders.forEach((c) => { - const item = new Folder(); - item.id = c.id; - item.name = c.name; - items.push(item); - }); - - expect(actual.length).toBeGreaterThan(0); - expect(actual).toEqual(JSON.stringify(items)); -} +import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction"; +import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; +import { VaultExportService } from "./vault-export.service"; +/** Tests the vault export service which handles exporting both individual and organizational vaults */ describe("VaultExportService", () => { - let exportService: IndividualVaultExportService; - let cryptoFunctionService: MockProxy; - let cipherService: MockProxy; - let pinService: MockProxy; - let folderService: MockProxy; - let keyService: MockProxy; - let encryptService: MockProxy; - let accountService: MockProxy; - let kdfConfigService: MockProxy; - let apiService: MockProxy; - let restrictedItemTypesService: Partial; + let service: VaultExportService; + let individualVaultExportService: MockProxy; + let organizationVaultExportService: MockProxy; + let accountService: FakeAccountService; + const mockUserId = Utils.newGuid() as UserId; + const mockOrganizationId = Utils.newGuid() as OrganizationId; beforeEach(() => { - cryptoFunctionService = mock(); - cipherService = mock(); - pinService = mock(); - folderService = mock(); - keyService = mock(); - encryptService = mock(); - accountService = mock(); - apiService = mock(); + individualVaultExportService = mock(); + organizationVaultExportService = mock(); + accountService = mockAccountServiceWith(mockUserId); - kdfConfigService = mock(); - - folderService.folderViews$.mockReturnValue(of(UserFolderViews)); - folderService.folders$.mockReturnValue(of(UserFolders)); - kdfConfigService.getKdfConfig.mockResolvedValue(DEFAULT_KDF_CONFIG); - encryptService.encryptString.mockResolvedValue(new EncString("encrypted")); - keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); - const userId = "" as UserId; - const accountInfo: AccountInfo = { - email: "", - emailVerified: true, - name: undefined, - }; - const activeAccount = { id: userId, ...accountInfo }; - accountService.activeAccount$ = new BehaviorSubject(activeAccount); - - restrictedItemTypesService = { - restricted$: new BehaviorSubject([]), - isCipherRestricted: jest.fn().mockReturnValue(false), - isCipherRestricted$: jest.fn().mockReturnValue(of(false)), - }; - - exportService = new IndividualVaultExportService( - folderService, - cipherService, - pinService, - keyService, - encryptService, - cryptoFunctionService, - kdfConfigService, + service = new VaultExportService( + individualVaultExportService, + organizationVaultExportService, accountService, - apiService, - restrictedItemTypesService as RestrictedItemTypesService, ); }); - it("exports unencrypted user ciphers", async () => { - cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); + describe("getExport", () => { + it("calls checkForImpersonation with userId", async () => { + const spy = jest.spyOn(service as any, "checkForImpersonation"); - const actual = await exportService.getExport("json"); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualCiphers(UserCipherViews.slice(0, 1), exportedData.data); - }); + await service.getExport(mockUserId, "json", ""); + expect(spy).toHaveBeenCalledWith(mockUserId); + }); - it("exports encrypted json user ciphers", async () => { - cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); + it("validates the given userId matches the current authenticated user", async () => { + const anotherUserId = "another-user-id" as UserId; - const actual = await exportService.getExport("encrypted_json"); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualCiphers(UserCipherDomains.slice(0, 1), exportedData.data); - }); + await expect(service.getExport(anotherUserId, "json", "")).rejects.toThrow( + "UserId does not match the currently authenticated user", + ); - it("does not unencrypted export trashed user items", async () => { - cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews); + expect(individualVaultExportService.getExport).not.toHaveBeenCalledWith(mockUserId, "json"); + }); - const actual = await exportService.getExport("json"); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualCiphers(UserCipherViews.slice(0, 2), exportedData.data); - }); + it("calls getExport when password is empty", async () => { + await service.getExport(mockUserId, "json", ""); + expect(individualVaultExportService.getExport).toHaveBeenCalledWith(mockUserId, "json"); + }); - it("does not encrypted export trashed user items", async () => { - cipherService.getAll.mockResolvedValue(UserCipherDomains); + it("throws error if format is csv and password is provided", async () => { + await expect(service.getExport(mockUserId, "csv", "secret")).rejects.toThrow( + "CSV does not support password protected export", + ); + expect(individualVaultExportService.getPasswordProtectedExport).not.toHaveBeenCalled(); + expect(individualVaultExportService.getExport).not.toHaveBeenCalled(); + }); - const actual = await exportService.getExport("encrypted_json"); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data); - }); + it("calls getPasswordProtectedExport when password is provided and format is not csv", async () => { + await service.getExport(mockUserId, "json", "somePassword"); + expect(individualVaultExportService.getPasswordProtectedExport).toHaveBeenCalledWith( + mockUserId, + "somePassword", + ); + }); - describe("password protected export", () => { - let exportedVault: ExportedVault; - let exportString: string; - let exportObject: any; - let mac: MockProxy; - let data: MockProxy; - const password = "password"; - const salt = "salt"; - - describe("export json object", () => { - beforeEach(async () => { - mac = mock(); - data = mock(); - - mac.encryptedString = "mac" as EncryptedString; - data.encryptedString = "encData" as EncryptedString; - - jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); - cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); - - exportedVault = await exportService.getPasswordProtectedExport(password); - expect(typeof exportedVault.data).toBe("string"); - exportString = (exportedVault as ExportedVaultAsString).data; - exportObject = JSON.parse(exportString); - }); - - it("specifies it is encrypted", () => { - expect(exportObject.encrypted).toBe(true); - }); - - it("specifies it's password protected", () => { - expect(exportObject.passwordProtected).toBe(true); - }); - - it("specifies salt", () => { - expect(exportObject.salt).toEqual("salt"); - }); - - it("specifies kdfIterations", () => { - expect(exportObject.kdfIterations).toEqual(PBKDF2KdfConfig.ITERATIONS.defaultValue); - }); - - it("has kdfType", () => { - expect(exportObject.kdfType).toEqual(KdfType.PBKDF2_SHA256); - }); - - it("has a mac property", async () => { - encryptService.encryptString.mockResolvedValue(mac); - - exportedVault = await exportService.getPasswordProtectedExport(password); - - expect(typeof exportedVault.data).toBe("string"); - exportString = (exportedVault as ExportedVaultAsString).data; - exportObject = JSON.parse(exportString); - expect(exportObject.encKeyValidation_DO_NOT_EDIT).toEqual(mac.encryptedString); - }); - - it("has data property", async () => { - encryptService.encryptString.mockResolvedValue(data); - - exportedVault = await exportService.getPasswordProtectedExport(password); - - expect(typeof exportedVault.data).toBe("string"); - exportString = (exportedVault as ExportedVaultAsString).data; - exportObject = JSON.parse(exportString); - expect(exportObject.data).toEqual(data.encryptedString); - }); - - it("encrypts the data property", async () => { - const unEncryptedExportVault = await exportService.getExport(); - - expect(typeof unEncryptedExportVault.data).toBe("string"); - const unEncryptedExportString = (unEncryptedExportVault as ExportedVaultAsString).data; - expect(exportObject.data).not.toEqual(unEncryptedExportString); - }); + it("uses default format csv if not provided", async () => { + await service.getExport(mockUserId); + expect(individualVaultExportService.getExport).toHaveBeenCalledWith(mockUserId, "csv"); }); }); - it("exported unencrypted object contains folders", async () => { - cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); + describe("getOrganizationExport", () => { + it("calls checkForImpersonation with userId", async () => { + const spy = jest.spyOn(service as any, "checkForImpersonation"); - const actual = await exportService.getExport("json"); + await service.getOrganizationExport(mockUserId, mockOrganizationId, "json", ""); + expect(spy).toHaveBeenCalledWith(mockUserId); + }); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualFolderViews(UserFolderViews, exportedData.data); - }); + it("validates the given userId matches the current authenticated user", async () => { + const anotherUserId = "another-user-id" as UserId; - it("exported encrypted json contains folders", async () => { - cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); + await expect( + service.getOrganizationExport(anotherUserId, mockOrganizationId, "json", ""), + ).rejects.toThrow("UserId does not match the currently authenticated user"); - const actual = await exportService.getExport("encrypted_json"); + expect(organizationVaultExportService.getOrganizationExport).not.toHaveBeenCalledWith( + mockUserId, + mockOrganizationId, + "json", + ); + }); - expect(typeof actual.data).toBe("string"); - const exportedData = actual as ExportedVaultAsString; - expectEqualFolders(UserFolders, exportedData.data); + it("calls getOrganizationExport when password is empty", async () => { + await service.getOrganizationExport(mockUserId, mockOrganizationId, "json", ""); + expect(organizationVaultExportService.getOrganizationExport).toHaveBeenCalledWith( + mockUserId, + mockOrganizationId, + "json", + false, + ); + }); + + it("throws error if format is csv and password is provided", async () => { + await expect( + service.getOrganizationExport(mockUserId, mockOrganizationId, "csv", "secret"), + ).rejects.toThrow("CSV does not support password protected export"); + expect(organizationVaultExportService.getPasswordProtectedExport).not.toHaveBeenCalled(); + expect(organizationVaultExportService.getOrganizationExport).not.toHaveBeenCalled(); + }); + + it("calls getPasswordProtectedExport when password is provided and format is not csv", async () => { + await service.getOrganizationExport(mockUserId, mockOrganizationId, "json", "somePassword"); + expect(organizationVaultExportService.getPasswordProtectedExport).toHaveBeenCalledWith( + mockUserId, + mockOrganizationId, + "somePassword", + false, + ); + }); + + it("when calling getOrganizationExport without a password it passes onlyManagedCollection param on", async () => { + await service.getOrganizationExport(mockUserId, mockOrganizationId, "json", "", true); + expect(organizationVaultExportService.getOrganizationExport).toHaveBeenCalledWith( + mockUserId, + mockOrganizationId, + "json", + true, + ); + }); + + it("when calling getOrganizationExport with a password it passes onlyManagedCollection param on", async () => { + await service.getOrganizationExport( + mockUserId, + mockOrganizationId, + "json", + "somePassword", + true, + ); + expect(organizationVaultExportService.getPasswordProtectedExport).toHaveBeenCalledWith( + mockUserId, + mockOrganizationId, + "somePassword", + true, + ); + }); }); }); - -export class FolderResponse { - id: string = null; - name: string = null; -} diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts index 9209587f4e..b601478d06 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/services/vault-export.service.ts @@ -1,5 +1,9 @@ +import { firstValueFrom } from "rxjs"; + +import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; +import { getUserId } from "@bitwarden/common/auth/services/account.service"; import { Utils } from "@bitwarden/common/platform/misc/utils"; -import { OrganizationId } from "@bitwarden/common/types/guid"; +import { UserId, OrganizationId } from "@bitwarden/common/types/guid"; import { ExportedVault } from "../types"; @@ -11,26 +15,35 @@ export class VaultExportService implements VaultExportServiceAbstraction { constructor( private individualVaultExportService: IndividualVaultExportServiceAbstraction, private organizationVaultExportService: OrganizationVaultExportServiceAbstraction, + private accountService: AccountService, ) {} /** Creates an export of an individual vault (My vault). Based on the provided format it will either be unencrypted, encrypted or password protected + * @param userId The userId of the account requesting the export * @param format The format of the export * @param password An optional password if the export should be password-protected * @returns The exported vault * @throws Error if the format is csv and a password is provided */ - async getExport(format: ExportFormat = "csv", password: string = ""): Promise { + async getExport( + userId: UserId, + format: ExportFormat = "csv", + password: string = "", + ): Promise { + await this.checkForImpersonation(userId); + if (!Utils.isNullOrWhitespace(password)) { if (format == "csv") { throw new Error("CSV does not support password protected export"); } - return this.individualVaultExportService.getPasswordProtectedExport(password); + return this.individualVaultExportService.getPasswordProtectedExport(userId, password); } - return this.individualVaultExportService.getExport(format); + return this.individualVaultExportService.getExport(userId, format); } /** Creates an export of an organizational vault. Based on the provided format it will either be unencrypted, encrypted or password protected + * @param userId The userId of the account requesting the export * @param organizationId The organization id * @param format The format of the export * @param password The password to protect the export @@ -43,17 +56,21 @@ export class VaultExportService implements VaultExportServiceAbstraction { * @throws Error if the organization policies prevent the export */ async getOrganizationExport( + userId: UserId, organizationId: OrganizationId, format: ExportFormat, password: string, onlyManagedCollections = false, ): Promise { + await this.checkForImpersonation(userId); + if (!Utils.isNullOrWhitespace(password)) { if (format == "csv") { throw new Error("CSV does not support password protected export"); } return this.organizationVaultExportService.getPasswordProtectedExport( + userId, organizationId, password, onlyManagedCollections, @@ -61,9 +78,21 @@ export class VaultExportService implements VaultExportServiceAbstraction { } return this.organizationVaultExportService.getOrganizationExport( + userId, organizationId, format, onlyManagedCollections, ); } + + /** Checks if the provided userId matches the currently authenticated user + * @param userId The userId to check + * @throws Error if the userId does not match the currently authenticated user + */ + private async checkForImpersonation(userId: UserId): Promise { + const currentUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); + if (userId !== currentUserId) { + throw new Error("UserId does not match the currently authenticated user"); + } + } } diff --git a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts index 550b421d27..f2caf4fe3f 100644 --- a/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts +++ b/libs/tools/export/vault-export/vault-export-ui/src/components/export.component.ts @@ -14,6 +14,7 @@ import { import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; import { combineLatest, + firstValueFrom, map, merge, Observable, @@ -460,9 +461,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit { } protected async getExportData(): Promise { + const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); return Utils.isNullOrWhitespace(this.organizationId) - ? this.exportService.getExport(this.format, this.filePassword) + ? this.exportService.getExport(userId, this.format, this.filePassword) : this.exportService.getOrganizationExport( + userId, this.organizationId, this.format, this.filePassword,