From ba817f0389677dbdd4f549195062f3b805b9e4c9 Mon Sep 17 00:00:00 2001 From: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Date: Wed, 17 Sep 2025 22:22:12 +0200 Subject: [PATCH] [PM-25503] Use org export api on encrypted and unencrypted org exports (#16290) * Introduce a new vault-export-api.service to replace the existing getOrganizationExport method in apiService * Use new vault-export-api.service instead of the ApiService to retrieve organizational export data * Remove unused method from apiService * Register VaultExportApiService on browser * Fxi linting issue by executing `npm run prettier` * Rename abstraction and implementation of VaultExportApiService * Use undefined instead of null * Rename file of default impl of vault-export-api-service * Fix test broken with 1bcdd80eeafe658f7bda4a5535f8e06c357406b8 * Define type for exportPromises --------- Co-authored-by: Daniel James Smith --- .../browser/src/background/main.background.ts | 7 +- .../service-container/service-container.ts | 7 +- .../src/services/jslib-services.module.ts | 9 +- libs/common/src/abstractions/api.service.ts | 2 - libs/common/src/services/api.service.ts | 12 -- .../vault-export-core/src/index.ts | 1 + .../api/default-vault-export-api.service.ts | 24 ++++ .../src/services/api/index.ts | 2 + .../vault-export-api.service.abstraction.ts | 13 ++ .../api/vault-export-api.service.spec.ts | 33 +++++ .../src/services/org-vault-export.service.ts | 129 +++++++++--------- 11 files changed, 159 insertions(+), 80 deletions(-) create mode 100644 libs/tools/export/vault-export/vault-export-core/src/services/api/default-vault-export-api.service.ts create mode 100644 libs/tools/export/vault-export/vault-export-core/src/services/api/index.ts create mode 100644 libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.abstraction.ts create mode 100644 libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.spec.ts diff --git a/apps/browser/src/background/main.background.ts b/apps/browser/src/background/main.background.ts index d52f1b9e35..cc3985fb39 100644 --- a/apps/browser/src/background/main.background.ts +++ b/apps/browser/src/background/main.background.ts @@ -256,6 +256,8 @@ import { IndividualVaultExportServiceAbstraction, OrganizationVaultExportService, OrganizationVaultExportServiceAbstraction, + DefaultVaultExportApiService, + VaultExportApiService, VaultExportService, VaultExportServiceAbstraction, } from "@bitwarden/vault-export-core"; @@ -362,6 +364,7 @@ export default class MainBackground { loginEmailService: LoginEmailServiceAbstraction; importApiService: ImportApiServiceAbstraction; importService: ImportServiceAbstraction; + exportApiService: VaultExportApiService; exportService: VaultExportServiceAbstraction; searchService: SearchServiceAbstraction; serverNotificationsService: ServerNotificationsService; @@ -1100,9 +1103,11 @@ export default class MainBackground { this.restrictedItemTypesService, ); + this.exportApiService = new DefaultVaultExportApiService(this.apiService); + this.organizationVaultExportService = new OrganizationVaultExportService( this.cipherService, - this.apiService, + this.exportApiService, this.pinService, this.keyService, this.encryptService, diff --git a/apps/cli/src/service-container/service-container.ts b/apps/cli/src/service-container/service-container.ts index b213f3ba1c..1e0aa1b15f 100644 --- a/apps/cli/src/service-container/service-container.ts +++ b/apps/cli/src/service-container/service-container.ts @@ -179,10 +179,12 @@ import { SerializedMemoryStorageService } from "@bitwarden/storage-core"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, + VaultExportApiService, OrganizationVaultExportService, OrganizationVaultExportServiceAbstraction, VaultExportService, VaultExportServiceAbstraction, + DefaultVaultExportApiService, } from "@bitwarden/vault-export-core"; import { CliBiometricsService } from "../key-management/cli-biometrics-service"; @@ -241,6 +243,7 @@ export class ServiceContainer { importService: ImportServiceAbstraction; importApiService: ImportApiServiceAbstraction; exportService: VaultExportServiceAbstraction; + vaultExportApiService: VaultExportApiService; individualExportService: IndividualVaultExportServiceAbstraction; organizationExportService: OrganizationVaultExportServiceAbstraction; searchService: SearchService; @@ -844,9 +847,11 @@ export class ServiceContainer { this.restrictedItemTypesService, ); + this.vaultExportApiService = new DefaultVaultExportApiService(this.apiService); + this.organizationExportService = new OrganizationVaultExportService( this.cipherService, - this.apiService, + this.vaultExportApiService, this.pinService, this.keyService, this.encryptService, diff --git a/libs/angular/src/services/jslib-services.module.ts b/libs/angular/src/services/jslib-services.module.ts index 3298e7e591..d08b4b17c2 100644 --- a/libs/angular/src/services/jslib-services.module.ts +++ b/libs/angular/src/services/jslib-services.module.ts @@ -343,6 +343,8 @@ import { PasswordRepromptService } from "@bitwarden/vault"; import { IndividualVaultExportService, IndividualVaultExportServiceAbstraction, + DefaultVaultExportApiService, + VaultExportApiService, OrganizationVaultExportService, OrganizationVaultExportServiceAbstraction, VaultExportService, @@ -887,12 +889,17 @@ const safeProviders: SafeProvider[] = [ RestrictedItemTypesService, ], }), + safeProvider({ + provide: VaultExportApiService, + useClass: DefaultVaultExportApiService, + deps: [ApiServiceAbstraction], + }), safeProvider({ provide: OrganizationVaultExportServiceAbstraction, useClass: OrganizationVaultExportService, deps: [ CipherServiceAbstraction, - ApiServiceAbstraction, + VaultExportApiService, PinServiceAbstraction, KeyService, EncryptService, diff --git a/libs/common/src/abstractions/api.service.ts b/libs/common/src/abstractions/api.service.ts index ab217c56fc..3372dda0d7 100644 --- a/libs/common/src/abstractions/api.service.ts +++ b/libs/common/src/abstractions/api.service.ts @@ -24,7 +24,6 @@ import { OrganizationConnectionConfigApis, OrganizationConnectionResponse, } from "../admin-console/models/response/organization-connection.response"; -import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response"; import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response"; import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response"; import { @@ -552,5 +551,4 @@ export abstract class ApiService { request: KeyConnectorUserKeyRequest, ): Promise; abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise; - abstract getOrganizationExport(organizationId: string): Promise; } diff --git a/libs/common/src/services/api.service.ts b/libs/common/src/services/api.service.ts index 3f24414bd2..8efe78b335 100644 --- a/libs/common/src/services/api.service.ts +++ b/libs/common/src/services/api.service.ts @@ -33,7 +33,6 @@ import { OrganizationConnectionConfigApis, OrganizationConnectionResponse, } from "../admin-console/models/response/organization-connection.response"; -import { OrganizationExportResponse } from "../admin-console/models/response/organization-export.response"; import { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response"; import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response"; import { @@ -1528,17 +1527,6 @@ export class ApiService implements ApiServiceAbstraction { } } - async getOrganizationExport(organizationId: string): Promise { - const r = await this.send( - "GET", - "/organizations/" + organizationId + "/export", - null, - true, - true, - ); - return new OrganizationExportResponse(r); - } - // Helpers async getActiveBearerToken(userId: UserId): Promise { diff --git a/libs/tools/export/vault-export/vault-export-core/src/index.ts b/libs/tools/export/vault-export/vault-export-core/src/index.ts index 46166750ad..181b4e4b23 100644 --- a/libs/tools/export/vault-export/vault-export-core/src/index.ts +++ b/libs/tools/export/vault-export/vault-export-core/src/index.ts @@ -7,3 +7,4 @@ export * from "./services/org-vault-export.service"; export * from "./services/individual-vault-export.service.abstraction"; export * from "./services/individual-vault-export.service"; export * from "./services/export-helper"; +export * from "./services/api"; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/api/default-vault-export-api.service.ts b/libs/tools/export/vault-export/vault-export-core/src/services/api/default-vault-export-api.service.ts new file mode 100644 index 0000000000..c3fca8f114 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-core/src/services/api/default-vault-export-api.service.ts @@ -0,0 +1,24 @@ +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationExportResponse } from "@bitwarden/common/admin-console/models/response/organization-export.response"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { VaultExportApiService } from "./vault-export-api.service.abstraction"; + +/** + * Service for handling vault export API interactions. + * @param apiService - An instance of {@link ApiService} used to make HTTP requests. + */ +export class DefaultVaultExportApiService implements VaultExportApiService { + constructor(private apiService: ApiService) {} + + async getOrganizationExport(organizationId: OrganizationId): Promise { + const r = await this.apiService.send( + "GET", + "/organizations/" + organizationId + "/export", + undefined, + true, + true, + ); + return new OrganizationExportResponse(r); + } +} diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/api/index.ts b/libs/tools/export/vault-export/vault-export-core/src/services/api/index.ts new file mode 100644 index 0000000000..4b0413dd1c --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-core/src/services/api/index.ts @@ -0,0 +1,2 @@ +export * from "./vault-export-api.service.abstraction"; +export * from "./default-vault-export-api.service"; diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.abstraction.ts b/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.abstraction.ts new file mode 100644 index 0000000000..40756dea29 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.abstraction.ts @@ -0,0 +1,13 @@ +import { OrganizationExportResponse } from "@bitwarden/common/admin-console/models/response/organization-export.response"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +export abstract class VaultExportApiService { + /** + * Retrieves the export data for a specific organization. + * @param organizationId The ID of the organization to export. + * @returns A promise that resolves to the organization export response. + */ + abstract getOrganizationExport( + organizationId: OrganizationId, + ): Promise; +} diff --git a/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.spec.ts b/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.spec.ts new file mode 100644 index 0000000000..275187a415 --- /dev/null +++ b/libs/tools/export/vault-export/vault-export-core/src/services/api/vault-export-api.service.spec.ts @@ -0,0 +1,33 @@ +import { mock, MockProxy } from "jest-mock-extended"; + +import { ApiService } from "@bitwarden/common/abstractions/api.service"; +import { OrganizationId } from "@bitwarden/common/types/guid"; + +import { DefaultVaultExportApiService } from "./default-vault-export-api.service"; +import { VaultExportApiService } from "./vault-export-api.service.abstraction"; + +describe("VaultExportApiService", () => { + let apiServiceMock: MockProxy; + let sut: VaultExportApiService; + + beforeEach(() => { + apiServiceMock = mock(); + sut = new DefaultVaultExportApiService(apiServiceMock); + }); + + it("should call apiService.send with correct parameters", async () => { + const orgId: OrganizationId = "test-org-id" as OrganizationId; + const apiResponse = { foo: "bar" }; + apiServiceMock.send.mockResolvedValue(apiResponse); + + await sut.getOrganizationExport(orgId); + + expect(apiServiceMock.send).toHaveBeenCalledWith( + "GET", + `/organizations/${orgId}/export`, + undefined, + true, + true, + ); + }); +}); 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 404a5d6cf3..a0a05f29ff 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,7 +10,6 @@ import { CollectionDetailsResponse, CollectionView, } from "@bitwarden/admin-console/common"; -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"; @@ -34,6 +33,7 @@ import { ExportedVaultAsString, } from "../types"; +import { VaultExportApiService } from "./api/vault-export-api.service.abstraction"; import { BaseVaultExportService } from "./base-vault-export.service"; import { ExportHelper } from "./export-helper"; import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; @@ -45,7 +45,7 @@ export class OrganizationVaultExportService { constructor( private cipherService: CipherService, - private apiService: ApiService, + private vaultExportApiService: VaultExportApiService, pinService: PinServiceAbstraction, private keyService: KeyService, encryptService: EncryptService, @@ -138,44 +138,49 @@ export class OrganizationVaultExportService const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); promises.push( - this.apiService.getOrganizationExport(organizationId).then((exportData) => { - const exportPromises: any = []; - if (exportData != null) { - if (exportData.collections != null && exportData.collections.length > 0) { - exportData.collections.forEach((c) => { - const collection = Collection.fromCollectionData( - new CollectionData(c as CollectionDetailsResponse), - ); - exportPromises.push( - firstValueFrom(this.keyService.activeUserOrgKeys$) - .then((keys) => - collection.decrypt(keys[organizationId as OrganizationId], this.encryptService), - ) - .then((decCol) => { - decCollections.push(decCol); - }), - ); - }); - } - if (exportData.ciphers != null && exportData.ciphers.length > 0) { - exportData.ciphers - .filter((c) => c.deletedDate === null) - .forEach(async (c) => { - const cipher = new Cipher(new CipherData(c)); + this.vaultExportApiService + .getOrganizationExport(organizationId as OrganizationId) + .then((exportData) => { + const exportPromises: Promise[] = []; + if (exportData != null) { + if (exportData.collections != null && exportData.collections.length > 0) { + exportData.collections.forEach((c) => { + const collection = Collection.fromCollectionData( + new CollectionData(c as CollectionDetailsResponse), + ); exportPromises.push( - this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { - if ( - !this.restrictedItemTypesService.isCipherRestricted(decCipher, restrictions) - ) { - decCiphers.push(decCipher); - } - }), + firstValueFrom(this.keyService.activeUserOrgKeys$) + .then((keys) => + collection.decrypt( + keys[organizationId as OrganizationId], + this.encryptService, + ), + ) + .then((decCol) => { + decCollections.push(decCol); + }), ); }); + } + if (exportData.ciphers != null && exportData.ciphers.length > 0) { + exportData.ciphers + .filter((c) => c.deletedDate === null) + .forEach(async (c) => { + const cipher = new Cipher(new CipherData(c)); + exportPromises.push( + this.cipherService.decrypt(cipher, activeUserId).then((decCipher) => { + if ( + !this.restrictedItemTypesService.isCipherRestricted(decCipher, restrictions) + ) { + decCiphers.push(decCipher); + } + }), + ); + }); + } } - } - return Promise.all(exportPromises); - }), + return Promise.all(exportPromises); + }), ); await Promise.all(promises); @@ -188,39 +193,37 @@ export class OrganizationVaultExportService private async getOrganizationEncryptedExport(organizationId: string): Promise { const collections: Collection[] = []; - let ciphers: Cipher[] = []; - const promises = []; - - promises.push( - this.apiService.getCollections(organizationId).then((c) => { - if (c != null && c.data != null && c.data.length > 0) { - c.data.forEach((r) => { - const collection = Collection.fromCollectionData( - new CollectionData(r as CollectionDetailsResponse), - ); - collections.push(collection); - }); - } - }), - ); + const ciphers: Cipher[] = []; const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); - promises.push( - this.apiService.getCiphersOrganization(organizationId).then((c) => { - if (c != null && c.data != null && c.data.length > 0) { - ciphers = c.data - .filter((item) => item.deletedDate === null) - .map((item) => new Cipher(new CipherData(item))) - .filter( - (cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions), - ); - } - }), + const exportData = await this.vaultExportApiService.getOrganizationExport( + organizationId as OrganizationId, ); - await Promise.all(promises); + if (exportData == null) { + return; + } + if (exportData.collections != null && exportData.collections.length > 0) { + exportData.collections.forEach((c) => { + const collection = Collection.fromCollectionData( + new CollectionData(c as CollectionDetailsResponse), + ); + collections.push(collection); + }); + } + + if (exportData.ciphers != null && exportData.ciphers.length > 0) { + exportData.ciphers + .filter((c) => c.deletedDate === null) + .forEach((c) => { + const cipher = new Cipher(new CipherData(c)); + if (!this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions)) { + ciphers.push(cipher); + } + }); + } return this.BuildEncryptedExport(organizationId, collections, ciphers); }