1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 22:33:35 +00:00

[PM-25918] Move required userId for export request up to component/command level (#14391)

* Require userId for KdfConfigService

* cleanup KdfConfigService unit tests

* Move required userId for export request up to component/command level

* Fix service creation/dependency injection

* Revert changes to kdf-config.service.spec cause by a bad rebase

* Fix linting issue

* Fix tests caused by bad rebase

* Validate provided userId to equal the current active user

* Create tests for vault-export.service

Deleted old tests which since have been replaced with individual-vault-export.service.spec.ts

---------

Co-authored-by: Thomas Avery <tavery@bitwarden.com>
Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2025-09-19 13:39:55 +02:00
committed by GitHub
parent a77fb354d8
commit 94764467e8
14 changed files with 220 additions and 387 deletions

View File

@@ -1097,7 +1097,6 @@ export default class MainBackground {
this.encryptService, this.encryptService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService,
this.apiService, this.apiService,
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
@@ -1113,13 +1112,13 @@ export default class MainBackground {
this.cryptoFunctionService, this.cryptoFunctionService,
this.collectionService, this.collectionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService,
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
this.exportService = new VaultExportService( this.exportService = new VaultExportService(
this.individualVaultExportService, this.individualVaultExportService,
this.organizationVaultExportService, this.organizationVaultExportService,
this.accountService,
); );
this.browserInitialInstallService = new BrowserInitialInstallService(this.stateProvider); this.browserInitialInstallService = new BrowserInitialInstallService(this.stateProvider);

View File

@@ -841,7 +841,6 @@ export class ServiceContainer {
this.encryptService, this.encryptService,
this.cryptoFunctionService, this.cryptoFunctionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService,
this.apiService, this.apiService,
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
@@ -857,13 +856,13 @@ export class ServiceContainer {
this.cryptoFunctionService, this.cryptoFunctionService,
this.collectionService, this.collectionService,
this.kdfConfigService, this.kdfConfigService,
this.accountService,
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
this.exportService = new VaultExportService( this.exportService = new VaultExportService(
this.individualExportService, this.individualExportService,
this.organizationExportService, this.organizationExportService,
this.accountService,
); );
this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService); this.userAutoUnlockKeyService = new UserAutoUnlockKeyService(this.keyService);

View File

@@ -70,10 +70,13 @@ export class ExportCommand {
password = await this.promptPassword(password); password = await this.promptPassword(password);
} }
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
exportContent = exportContent =
options.organizationid == null options.organizationid == null
? await this.exportService.getExport(format, password) ? await this.exportService.getExport(userId, format, password)
: await this.exportService.getOrganizationExport( : await this.exportService.getOrganizationExport(
userId,
options.organizationid, options.organizationid,
format, format,
password, password,

View File

@@ -884,7 +884,6 @@ const safeProviders: SafeProvider[] = [
EncryptService, EncryptService,
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
KdfConfigService, KdfConfigService,
AccountServiceAbstraction,
ApiServiceAbstraction, ApiServiceAbstraction,
RestrictedItemTypesService, RestrictedItemTypesService,
], ],
@@ -906,14 +905,17 @@ const safeProviders: SafeProvider[] = [
CryptoFunctionServiceAbstraction, CryptoFunctionServiceAbstraction,
CollectionService, CollectionService,
KdfConfigService, KdfConfigService,
AccountServiceAbstraction,
RestrictedItemTypesService, RestrictedItemTypesService,
], ],
}), }),
safeProvider({ safeProvider({
provide: VaultExportServiceAbstraction, provide: VaultExportServiceAbstraction,
useClass: VaultExportService, useClass: VaultExportService,
deps: [IndividualVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction], deps: [
IndividualVaultExportServiceAbstraction,
OrganizationVaultExportServiceAbstraction,
AccountServiceAbstraction,
],
}), }),
safeProvider({ safeProvider({
provide: SearchServiceAbstraction, provide: SearchServiceAbstraction,

View File

@@ -7,7 +7,10 @@ module.exports = {
testMatch: ["**/+(*.)+(spec).+(ts)"], testMatch: ["**/+(*.)+(spec).+(ts)"],
preset: "ts-jest", preset: "ts-jest",
testEnvironment: "jsdom", testEnvironment: "jsdom",
moduleNameMapper: pathsToModuleNameMapper(compilerOptions?.paths || {}, { moduleNameMapper: pathsToModuleNameMapper(
{ "@bitwarden/common/spec": ["libs/common/spec"], ...(compilerOptions?.paths ?? {}) },
{
prefix: "<rootDir>/../../../../../", prefix: "<rootDir>/../../../../../",
}), },
),
}; };

View File

@@ -1,8 +1,10 @@
import { UserId } from "@bitwarden/common/types/guid";
import { ExportedVault } from "../types"; import { ExportedVault } from "../types";
import { ExportFormat } from "./vault-export.service.abstraction"; import { ExportFormat } from "./vault-export.service.abstraction";
export abstract class IndividualVaultExportServiceAbstraction { export abstract class IndividualVaultExportServiceAbstraction {
abstract getExport: (format: ExportFormat) => Promise<ExportedVault>; abstract getExport: (userId: UserId, format: ExportFormat) => Promise<ExportedVault>;
abstract getPasswordProtectedExport: (password: string) => Promise<ExportedVault>; abstract getPasswordProtectedExport: (userId: UserId, password: string) => Promise<ExportedVault>;
} }

View File

@@ -3,7 +3,6 @@ import * as JSZip from "jszip";
import { BehaviorSubject, of } from "rxjs"; import { BehaviorSubject, of } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { import {
@@ -175,7 +174,6 @@ describe("VaultExportService", () => {
let keyService: MockProxy<KeyService>; let keyService: MockProxy<KeyService>;
let encryptService: MockProxy<EncryptService>; let encryptService: MockProxy<EncryptService>;
let kdfConfigService: MockProxy<KdfConfigService>; let kdfConfigService: MockProxy<KdfConfigService>;
let accountService: MockProxy<AccountService>;
let apiService: MockProxy<ApiService>; let apiService: MockProxy<ApiService>;
let restrictedSubject: BehaviorSubject<RestrictedCipherType[]>; let restrictedSubject: BehaviorSubject<RestrictedCipherType[]>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>; let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
@@ -191,7 +189,6 @@ describe("VaultExportService", () => {
keyService = mock<KeyService>(); keyService = mock<KeyService>();
encryptService = mock<EncryptService>(); encryptService = mock<EncryptService>();
kdfConfigService = mock<KdfConfigService>(); kdfConfigService = mock<KdfConfigService>();
accountService = mock<AccountService>();
apiService = mock<ApiService>(); apiService = mock<ApiService>();
keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any)); keyService.userKey$.mockReturnValue(new BehaviorSubject("mockOriginalUserKey" as any));
@@ -202,14 +199,6 @@ describe("VaultExportService", () => {
isCipherRestricted$: jest.fn().mockReturnValue(of(false)), 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({}); fetchMock = jest.fn().mockResolvedValue({});
global.fetch = fetchMock; global.fetch = fetchMock;
@@ -236,7 +225,6 @@ describe("VaultExportService", () => {
encryptService, encryptService,
cryptoFunctionService, cryptoFunctionService,
kdfConfigService, kdfConfigService,
accountService,
apiService, apiService,
restrictedItemTypesService as RestrictedItemTypesService, restrictedItemTypesService as RestrictedItemTypesService,
); );
@@ -245,7 +233,7 @@ describe("VaultExportService", () => {
it("exports unencrypted user ciphers", async () => { it("exports unencrypted user ciphers", async () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); 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"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
expectEqualCiphers(UserCipherViews.slice(0, 1), exportedData.data); expectEqualCiphers(UserCipherViews.slice(0, 1), exportedData.data);
@@ -254,7 +242,7 @@ describe("VaultExportService", () => {
it("exports encrypted json user ciphers", async () => { it("exports encrypted json user ciphers", async () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); 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"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
expectEqualCiphers(UserCipherDomains.slice(0, 1), exportedData.data); expectEqualCiphers(UserCipherDomains.slice(0, 1), exportedData.data);
@@ -263,7 +251,7 @@ describe("VaultExportService", () => {
it("does not unencrypted export trashed user items", async () => { it("does not unencrypted export trashed user items", async () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews);
const actual = await exportService.getExport("json"); const actual = await exportService.getExport(userId, "json");
expect(typeof actual.data).toBe("string"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
expectEqualCiphers(UserCipherViews.slice(0, 2), exportedData.data); expectEqualCiphers(UserCipherViews.slice(0, 2), exportedData.data);
@@ -272,7 +260,7 @@ describe("VaultExportService", () => {
it("does not encrypted export trashed user items", async () => { it("does not encrypted export trashed user items", async () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains); 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"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data); expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data);
@@ -291,7 +279,7 @@ describe("VaultExportService", () => {
const testCiphers = [UserCipherViews[0], cardCipher, UserCipherViews[1]]; const testCiphers = [UserCipherViews[0], cardCipher, UserCipherViews[1]];
cipherService.getAllDecrypted.mockResolvedValue(testCiphers); cipherService.getAllDecrypted.mockResolvedValue(testCiphers);
const actual = await exportService.getExport("json"); const actual = await exportService.getExport(userId, "json");
expect(typeof actual.data).toBe("string"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
@@ -311,7 +299,7 @@ describe("VaultExportService", () => {
const testCiphers = [UserCipherDomains[0], cardCipher, UserCipherDomains[1]]; const testCiphers = [UserCipherDomains[0], cardCipher, UserCipherDomains[1]];
cipherService.getAll.mockResolvedValue(testCiphers); 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"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
@@ -323,7 +311,7 @@ describe("VaultExportService", () => {
cipherService.getAllDecrypted.mockResolvedValue([]); cipherService.getAllDecrypted.mockResolvedValue([]);
folderService.getAllDecryptedFromState.mockResolvedValue([]); folderService.getAllDecryptedFromState.mockResolvedValue([]);
const exportedVault = await exportService.getExport("zip"); const exportedVault = await exportService.getExport(userId, "zip");
expect(exportedVault.type).toBe("application/zip"); expect(exportedVault.type).toBe("application/zip");
const exportZip = exportedVault as ExportedVaultAsBlob; const exportZip = exportedVault as ExportedVaultAsBlob;
@@ -348,7 +336,7 @@ describe("VaultExportService", () => {
cipherService.getAllDecrypted.mockResolvedValue([cipherView, orgCipherView]); cipherService.getAllDecrypted.mockResolvedValue([cipherView, orgCipherView]);
folderService.getAllDecryptedFromState.mockResolvedValue([]); folderService.getAllDecryptedFromState.mockResolvedValue([]);
const exportedVault = await exportService.getExport("zip"); const exportedVault = await exportService.getExport(userId, "zip");
const zip = await JSZip.loadAsync(exportedVault.data); const zip = await JSZip.loadAsync(exportedVault.data);
const data = await zip.file("data.json")?.async("string"); const data = await zip.file("data.json")?.async("string");
@@ -380,7 +368,7 @@ describe("VaultExportService", () => {
global.Request = jest.fn(() => {}) as any; global.Request = jest.fn(() => {}) as any;
await expect(async () => { await expect(async () => {
await exportService.getExport("zip"); await exportService.getExport(userId, "zip");
}).rejects.toThrow("Error downloading attachment"); }).rejects.toThrow("Error downloading attachment");
}, },
); );
@@ -408,7 +396,7 @@ describe("VaultExportService", () => {
global.Request = jest.fn(() => {}) as any; global.Request = jest.fn(() => {}) as any;
await expect(async () => { await expect(async () => {
await exportService.getExport("zip"); await exportService.getExport(userId, "zip");
}).rejects.toThrow("Error decrypting attachment"); }).rejects.toThrow("Error decrypting attachment");
}); });
@@ -434,7 +422,7 @@ describe("VaultExportService", () => {
) as any; ) as any;
global.Request = jest.fn(() => {}) 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"); expect(exportedVault.type).toBe("application/zip");
const exportZip = exportedVault as ExportedVaultAsBlob; const exportZip = exportedVault as ExportedVaultAsBlob;
@@ -464,7 +452,7 @@ describe("VaultExportService", () => {
jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt); jest.spyOn(Utils, "fromBufferToB64").mockReturnValue(salt);
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1));
exportedVault = await exportService.getPasswordProtectedExport(password); exportedVault = await exportService.getPasswordProtectedExport(userId, password);
exportString = exportedVault.data; exportString = exportedVault.data;
exportObject = JSON.parse(exportString); exportObject = JSON.parse(exportString);
}); });
@@ -491,7 +479,7 @@ describe("VaultExportService", () => {
it("has a mac property", async () => { it("has a mac property", async () => {
encryptService.encryptString.mockResolvedValue(mac); encryptService.encryptString.mockResolvedValue(mac);
exportedVault = await exportService.getPasswordProtectedExport(password); exportedVault = await exportService.getPasswordProtectedExport(userId, password);
exportString = exportedVault.data; exportString = exportedVault.data;
exportObject = JSON.parse(exportString); exportObject = JSON.parse(exportString);
@@ -500,7 +488,7 @@ describe("VaultExportService", () => {
it("has data property", async () => { it("has data property", async () => {
encryptService.encryptString.mockResolvedValue(data); encryptService.encryptString.mockResolvedValue(data);
exportedVault = await exportService.getPasswordProtectedExport(password); exportedVault = await exportService.getPasswordProtectedExport(userId, password);
exportString = exportedVault.data; exportString = exportedVault.data;
exportObject = JSON.parse(exportString); exportObject = JSON.parse(exportString);
@@ -508,7 +496,7 @@ describe("VaultExportService", () => {
}); });
it("encrypts the data property", async () => { it("encrypts the data property", async () => {
const unEncryptedExportVault = await exportService.getExport(); const unEncryptedExportVault = await exportService.getExport(userId);
const unEncryptedExportString = unEncryptedExportVault.data; const unEncryptedExportString = unEncryptedExportVault.data;
expect(exportObject.data).not.toEqual(unEncryptedExportString); expect(exportObject.data).not.toEqual(unEncryptedExportString);
@@ -520,7 +508,7 @@ describe("VaultExportService", () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1));
folderService.folderViews$.mockReturnValue(of(UserFolderViews)); folderService.folderViews$.mockReturnValue(of(UserFolderViews));
const actual = await exportService.getExport("json"); const actual = await exportService.getExport(userId, "json");
expect(typeof actual.data).toBe("string"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;
@@ -531,7 +519,7 @@ describe("VaultExportService", () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1));
folderService.folders$.mockReturnValue(of(UserFolders)); 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"); expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString; const exportedData = actual as ExportedVaultAsString;

View File

@@ -5,8 +5,6 @@ import * as papa from "papaparse";
import { firstValueFrom } from "rxjs"; import { firstValueFrom } from "rxjs";
import { ApiService } from "@bitwarden/common/abstractions/api.service"; 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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
@@ -49,7 +47,6 @@ export class IndividualVaultExportService
encryptService: EncryptService, encryptService: EncryptService,
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
private accountService: AccountService,
private apiService: ApiService, private apiService: ApiService,
private restrictedItemTypesService: RestrictedItemTypesService, 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 /** 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 * @param format The format of the export
*/ */
async getExport(format: ExportFormat = "csv"): Promise<ExportedVault> { async getExport(userId: UserId, format: ExportFormat = "csv"): Promise<ExportedVault> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (format === "encrypted_json") { if (format === "encrypted_json") {
return this.getEncryptedExport(userId); return this.getEncryptedExport(userId);
} else if (format === "zip") { } 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 /** 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 * @param password The password to encrypt the export with
* @returns A password-protected encrypted individual vault export * @returns A password-protected encrypted individual vault export
*/ */
async getPasswordProtectedExport(password: string): Promise<ExportedVaultAsString> { async getPasswordProtectedExport(
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId)); userId: UserId,
const exportVault = await this.getExport("json"); password: string,
): Promise<ExportedVaultAsString> {
const exportVault = await this.getExport(userId, "json");
if (exportVault.type !== "text/plain") { if (exportVault.type !== "text/plain") {
throw new Error("Unexpected export type"); throw new Error("Unexpected export type");

View File

@@ -1,4 +1,4 @@
import { OrganizationId } from "@bitwarden/common/types/guid"; import { UserId, OrganizationId } from "@bitwarden/common/types/guid";
import { ExportedVaultAsString } from "../types"; import { ExportedVaultAsString } from "../types";
@@ -6,11 +6,13 @@ import { ExportFormat } from "./vault-export.service.abstraction";
export abstract class OrganizationVaultExportServiceAbstraction { export abstract class OrganizationVaultExportServiceAbstraction {
abstract getPasswordProtectedExport: ( abstract getPasswordProtectedExport: (
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
password: string, password: string,
onlyManagedCollections: boolean, onlyManagedCollections: boolean,
) => Promise<ExportedVaultAsString>; ) => Promise<ExportedVaultAsString>;
abstract getOrganizationExport: ( abstract getOrganizationExport: (
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
format: ExportFormat, format: ExportFormat,
onlyManagedCollections: boolean, onlyManagedCollections: boolean,

View File

@@ -10,8 +10,6 @@ import {
CollectionDetailsResponse, CollectionDetailsResponse,
CollectionView, CollectionView,
} from "@bitwarden/admin-console/common"; } 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 { CryptoFunctionService } from "@bitwarden/common/key-management/crypto/abstractions/crypto-function.service";
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service"; import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction"; import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
@@ -52,25 +50,26 @@ export class OrganizationVaultExportService
cryptoFunctionService: CryptoFunctionService, cryptoFunctionService: CryptoFunctionService,
private collectionService: CollectionService, private collectionService: CollectionService,
kdfConfigService: KdfConfigService, kdfConfigService: KdfConfigService,
private accountService: AccountService,
private restrictedItemTypesService: RestrictedItemTypesService, private restrictedItemTypesService: RestrictedItemTypesService,
) { ) {
super(pinService, encryptService, cryptoFunctionService, kdfConfigService); super(pinService, encryptService, cryptoFunctionService, kdfConfigService);
} }
/** Creates a password protected export of an organizational vault. /** 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 organizationId The organization id
* @param password The password to protect the export * @param password The password to protect the export
* @param onlyManagedCollections If true only managed collections will be exported * @param onlyManagedCollections If true only managed collections will be exported
* @returns The exported vault * @returns The exported vault
*/ */
async getPasswordProtectedExport( async getPasswordProtectedExport(
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
password: string, password: string,
onlyManagedCollections: boolean, onlyManagedCollections: boolean,
): Promise<ExportedVaultAsString> { ): Promise<ExportedVaultAsString> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
const exportVault = await this.getOrganizationExport( const exportVault = await this.getOrganizationExport(
userId,
organizationId, organizationId,
"json", "json",
onlyManagedCollections, 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 /** 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 organizationId The organization id
* @param format The format of the export * @param format The format of the export
* @param onlyManagedCollections If true only managed collections will be exported * @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 * @throws Error if the organization policies prevent the export
*/ */
async getOrganizationExport( async getOrganizationExport(
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
format: ExportFormat = "csv", format: ExportFormat = "csv",
onlyManagedCollections: boolean, onlyManagedCollections: boolean,
@@ -105,7 +106,6 @@ export class OrganizationVaultExportService
if (format === "zip") { if (format === "zip") {
throw new Error("Zip export not supported for organization"); throw new Error("Zip export not supported for organization");
} }
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (format === "encrypted_json") { if (format === "encrypted_json") {
return { return {

View File

@@ -1,4 +1,4 @@
import { OrganizationId } from "@bitwarden/common/types/guid"; import { UserId, OrganizationId } from "@bitwarden/common/types/guid";
import { ExportedVault } from "../types"; 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 type ExportFormat = (typeof EXPORT_FORMATS)[number];
export abstract class VaultExportServiceAbstraction { export abstract class VaultExportServiceAbstraction {
abstract getExport: (format: ExportFormat, password: string) => Promise<ExportedVault>; abstract getExport: (
userId: UserId,
format: ExportFormat,
password: string,
) => Promise<ExportedVault>;
abstract getOrganizationExport: ( abstract getOrganizationExport: (
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
format: ExportFormat, format: ExportFormat,
password: string, password: string,

View File

@@ -1,355 +1,153 @@
import { mock, MockProxy } from "jest-mock-extended"; 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 { Utils } from "@bitwarden/common/platform/misc/utils";
import { UserId } from "@bitwarden/common/types/guid"; import { FakeAccountService, mockAccountServiceWith } from "@bitwarden/common/spec";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service"; import { OrganizationId, UserId } from "@bitwarden/common/types/guid";
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 { BuildTestObject, GetUniqueString } from "../../../../../../common/spec"; import { IndividualVaultExportServiceAbstraction } from "./individual-vault-export.service.abstraction";
import { ExportedVault, ExportedVaultAsString } from "../types"; import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction";
import { VaultExportService } from "./vault-export.service";
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<LoginView>(
{
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<Login>(
{
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));
}
/** Tests the vault export service which handles exporting both individual and organizational vaults */
describe("VaultExportService", () => { describe("VaultExportService", () => {
let exportService: IndividualVaultExportService; let service: VaultExportService;
let cryptoFunctionService: MockProxy<CryptoFunctionService>; let individualVaultExportService: MockProxy<IndividualVaultExportServiceAbstraction>;
let cipherService: MockProxy<CipherService>; let organizationVaultExportService: MockProxy<OrganizationVaultExportServiceAbstraction>;
let pinService: MockProxy<PinServiceAbstraction>; let accountService: FakeAccountService;
let folderService: MockProxy<FolderService>; const mockUserId = Utils.newGuid() as UserId;
let keyService: MockProxy<KeyService>; const mockOrganizationId = Utils.newGuid() as OrganizationId;
let encryptService: MockProxy<EncryptService>;
let accountService: MockProxy<AccountService>;
let kdfConfigService: MockProxy<KdfConfigService>;
let apiService: MockProxy<ApiService>;
let restrictedItemTypesService: Partial<RestrictedItemTypesService>;
beforeEach(() => { beforeEach(() => {
cryptoFunctionService = mock<CryptoFunctionService>(); individualVaultExportService = mock<IndividualVaultExportServiceAbstraction>();
cipherService = mock<CipherService>(); organizationVaultExportService = mock<OrganizationVaultExportServiceAbstraction>();
pinService = mock<PinServiceAbstraction>(); accountService = mockAccountServiceWith(mockUserId);
folderService = mock<FolderService>();
keyService = mock<KeyService>();
encryptService = mock<EncryptService>();
accountService = mock<AccountService>();
apiService = mock<ApiService>();
kdfConfigService = mock<KdfConfigService>(); service = new VaultExportService(
individualVaultExportService,
folderService.folderViews$.mockReturnValue(of(UserFolderViews)); organizationVaultExportService,
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<RestrictedCipherType[]>([]),
isCipherRestricted: jest.fn().mockReturnValue(false),
isCipherRestricted$: jest.fn().mockReturnValue(of(false)),
};
exportService = new IndividualVaultExportService(
folderService,
cipherService,
pinService,
keyService,
encryptService,
cryptoFunctionService,
kdfConfigService,
accountService, accountService,
apiService,
restrictedItemTypesService as RestrictedItemTypesService,
); );
}); });
it("exports unencrypted user ciphers", async () => { describe("getExport", () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1)); it("calls checkForImpersonation with userId", async () => {
const spy = jest.spyOn(service as any, "checkForImpersonation");
const actual = await exportService.getExport("json"); await service.getExport(mockUserId, "json", "");
expect(typeof actual.data).toBe("string"); expect(spy).toHaveBeenCalledWith(mockUserId);
const exportedData = actual as ExportedVaultAsString;
expectEqualCiphers(UserCipherViews.slice(0, 1), exportedData.data);
}); });
it("exports encrypted json user ciphers", async () => { it("validates the given userId matches the current authenticated user", async () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1)); const anotherUserId = "another-user-id" as UserId;
const actual = await exportService.getExport("encrypted_json"); await expect(service.getExport(anotherUserId, "json", "")).rejects.toThrow(
expect(typeof actual.data).toBe("string"); "UserId does not match the currently authenticated user",
const exportedData = actual as ExportedVaultAsString; );
expectEqualCiphers(UserCipherDomains.slice(0, 1), exportedData.data);
expect(individualVaultExportService.getExport).not.toHaveBeenCalledWith(mockUserId, "json");
}); });
it("does not unencrypted export trashed user items", async () => { it("calls getExport when password is empty", async () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews); await service.getExport(mockUserId, "json", "");
expect(individualVaultExportService.getExport).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("does not encrypted export trashed user items", async () => { it("throws error if format is csv and password is provided", async () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains); await expect(service.getExport(mockUserId, "csv", "secret")).rejects.toThrow(
"CSV does not support password protected export",
const actual = await exportService.getExport("encrypted_json"); );
expect(typeof actual.data).toBe("string"); expect(individualVaultExportService.getPasswordProtectedExport).not.toHaveBeenCalled();
const exportedData = actual as ExportedVaultAsString; expect(individualVaultExportService.getExport).not.toHaveBeenCalled();
expectEqualCiphers(UserCipherDomains.slice(0, 2), exportedData.data);
}); });
describe("password protected export", () => { it("calls getPasswordProtectedExport when password is provided and format is not csv", async () => {
let exportedVault: ExportedVault; await service.getExport(mockUserId, "json", "somePassword");
let exportString: string; expect(individualVaultExportService.getPasswordProtectedExport).toHaveBeenCalledWith(
let exportObject: any; mockUserId,
let mac: MockProxy<EncString>; "somePassword",
let data: MockProxy<EncString>; );
const password = "password";
const salt = "salt";
describe("export json object", () => {
beforeEach(async () => {
mac = mock<EncString>();
data = mock<EncString>();
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", () => { it("uses default format csv if not provided", async () => {
expect(exportObject.encrypted).toBe(true); await service.getExport(mockUserId);
}); expect(individualVaultExportService.getExport).toHaveBeenCalledWith(mockUserId, "csv");
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("exported unencrypted object contains folders", async () => {
cipherService.getAllDecrypted.mockResolvedValue(UserCipherViews.slice(0, 1));
const actual = await exportService.getExport("json");
expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString;
expectEqualFolderViews(UserFolderViews, exportedData.data);
});
it("exported encrypted json contains folders", async () => {
cipherService.getAll.mockResolvedValue(UserCipherDomains.slice(0, 1));
const actual = await exportService.getExport("encrypted_json");
expect(typeof actual.data).toBe("string");
const exportedData = actual as ExportedVaultAsString;
expectEqualFolders(UserFolders, exportedData.data);
}); });
}); });
export class FolderResponse { describe("getOrganizationExport", () => {
id: string = null; it("calls checkForImpersonation with userId", async () => {
name: string = null; const spy = jest.spyOn(service as any, "checkForImpersonation");
}
await service.getOrganizationExport(mockUserId, mockOrganizationId, "json", "");
expect(spy).toHaveBeenCalledWith(mockUserId);
});
it("validates the given userId matches the current authenticated user", async () => {
const anotherUserId = "another-user-id" as UserId;
await expect(
service.getOrganizationExport(anotherUserId, mockOrganizationId, "json", ""),
).rejects.toThrow("UserId does not match the currently authenticated user");
expect(organizationVaultExportService.getOrganizationExport).not.toHaveBeenCalledWith(
mockUserId,
mockOrganizationId,
"json",
);
});
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,
);
});
});
});

View File

@@ -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 { 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"; import { ExportedVault } from "../types";
@@ -11,26 +15,35 @@ export class VaultExportService implements VaultExportServiceAbstraction {
constructor( constructor(
private individualVaultExportService: IndividualVaultExportServiceAbstraction, private individualVaultExportService: IndividualVaultExportServiceAbstraction,
private organizationVaultExportService: OrganizationVaultExportServiceAbstraction, 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 /** 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 format The format of the export
* @param password An optional password if the export should be password-protected * @param password An optional password if the export should be password-protected
* @returns The exported vault * @returns The exported vault
* @throws Error if the format is csv and a password is provided * @throws Error if the format is csv and a password is provided
*/ */
async getExport(format: ExportFormat = "csv", password: string = ""): Promise<ExportedVault> { async getExport(
userId: UserId,
format: ExportFormat = "csv",
password: string = "",
): Promise<ExportedVault> {
await this.checkForImpersonation(userId);
if (!Utils.isNullOrWhitespace(password)) { if (!Utils.isNullOrWhitespace(password)) {
if (format == "csv") { if (format == "csv") {
throw new Error("CSV does not support password protected export"); 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 /** 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 organizationId The organization id
* @param format The format of the export * @param format The format of the export
* @param password The password to protect 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 * @throws Error if the organization policies prevent the export
*/ */
async getOrganizationExport( async getOrganizationExport(
userId: UserId,
organizationId: OrganizationId, organizationId: OrganizationId,
format: ExportFormat, format: ExportFormat,
password: string, password: string,
onlyManagedCollections = false, onlyManagedCollections = false,
): Promise<ExportedVault> { ): Promise<ExportedVault> {
await this.checkForImpersonation(userId);
if (!Utils.isNullOrWhitespace(password)) { if (!Utils.isNullOrWhitespace(password)) {
if (format == "csv") { if (format == "csv") {
throw new Error("CSV does not support password protected export"); throw new Error("CSV does not support password protected export");
} }
return this.organizationVaultExportService.getPasswordProtectedExport( return this.organizationVaultExportService.getPasswordProtectedExport(
userId,
organizationId, organizationId,
password, password,
onlyManagedCollections, onlyManagedCollections,
@@ -61,9 +78,21 @@ export class VaultExportService implements VaultExportServiceAbstraction {
} }
return this.organizationVaultExportService.getOrganizationExport( return this.organizationVaultExportService.getOrganizationExport(
userId,
organizationId, organizationId,
format, format,
onlyManagedCollections, 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<void> {
const currentUserId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
if (userId !== currentUserId) {
throw new Error("UserId does not match the currently authenticated user");
}
}
} }

View File

@@ -14,6 +14,7 @@ import {
import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms"; import { ReactiveFormsModule, UntypedFormBuilder, Validators } from "@angular/forms";
import { import {
combineLatest, combineLatest,
firstValueFrom,
map, map,
merge, merge,
Observable, Observable,
@@ -460,9 +461,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
} }
protected async getExportData(): Promise<ExportedVault> { protected async getExportData(): Promise<ExportedVault> {
const userId = await firstValueFrom(this.accountService.activeAccount$.pipe(getUserId));
return Utils.isNullOrWhitespace(this.organizationId) return Utils.isNullOrWhitespace(this.organizationId)
? this.exportService.getExport(this.format, this.filePassword) ? this.exportService.getExport(userId, this.format, this.filePassword)
: this.exportService.getOrganizationExport( : this.exportService.getOrganizationExport(
userId,
this.organizationId, this.organizationId,
this.format, this.format,
this.filePassword, this.filePassword,