1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-12 06:13:38 +00:00

[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 1bcdd80eea

* Define type for exportPromises

---------

Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
Daniel James Smith
2025-09-17 22:22:12 +02:00
committed by GitHub
parent 200fc71c5c
commit ba817f0389
11 changed files with 159 additions and 80 deletions

View File

@@ -256,6 +256,8 @@ import {
IndividualVaultExportServiceAbstraction, IndividualVaultExportServiceAbstraction,
OrganizationVaultExportService, OrganizationVaultExportService,
OrganizationVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction,
DefaultVaultExportApiService,
VaultExportApiService,
VaultExportService, VaultExportService,
VaultExportServiceAbstraction, VaultExportServiceAbstraction,
} from "@bitwarden/vault-export-core"; } from "@bitwarden/vault-export-core";
@@ -362,6 +364,7 @@ export default class MainBackground {
loginEmailService: LoginEmailServiceAbstraction; loginEmailService: LoginEmailServiceAbstraction;
importApiService: ImportApiServiceAbstraction; importApiService: ImportApiServiceAbstraction;
importService: ImportServiceAbstraction; importService: ImportServiceAbstraction;
exportApiService: VaultExportApiService;
exportService: VaultExportServiceAbstraction; exportService: VaultExportServiceAbstraction;
searchService: SearchServiceAbstraction; searchService: SearchServiceAbstraction;
serverNotificationsService: ServerNotificationsService; serverNotificationsService: ServerNotificationsService;
@@ -1100,9 +1103,11 @@ export default class MainBackground {
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
this.exportApiService = new DefaultVaultExportApiService(this.apiService);
this.organizationVaultExportService = new OrganizationVaultExportService( this.organizationVaultExportService = new OrganizationVaultExportService(
this.cipherService, this.cipherService,
this.apiService, this.exportApiService,
this.pinService, this.pinService,
this.keyService, this.keyService,
this.encryptService, this.encryptService,

View File

@@ -179,10 +179,12 @@ import { SerializedMemoryStorageService } from "@bitwarden/storage-core";
import { import {
IndividualVaultExportService, IndividualVaultExportService,
IndividualVaultExportServiceAbstraction, IndividualVaultExportServiceAbstraction,
VaultExportApiService,
OrganizationVaultExportService, OrganizationVaultExportService,
OrganizationVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction,
VaultExportService, VaultExportService,
VaultExportServiceAbstraction, VaultExportServiceAbstraction,
DefaultVaultExportApiService,
} from "@bitwarden/vault-export-core"; } from "@bitwarden/vault-export-core";
import { CliBiometricsService } from "../key-management/cli-biometrics-service"; import { CliBiometricsService } from "../key-management/cli-biometrics-service";
@@ -241,6 +243,7 @@ export class ServiceContainer {
importService: ImportServiceAbstraction; importService: ImportServiceAbstraction;
importApiService: ImportApiServiceAbstraction; importApiService: ImportApiServiceAbstraction;
exportService: VaultExportServiceAbstraction; exportService: VaultExportServiceAbstraction;
vaultExportApiService: VaultExportApiService;
individualExportService: IndividualVaultExportServiceAbstraction; individualExportService: IndividualVaultExportServiceAbstraction;
organizationExportService: OrganizationVaultExportServiceAbstraction; organizationExportService: OrganizationVaultExportServiceAbstraction;
searchService: SearchService; searchService: SearchService;
@@ -844,9 +847,11 @@ export class ServiceContainer {
this.restrictedItemTypesService, this.restrictedItemTypesService,
); );
this.vaultExportApiService = new DefaultVaultExportApiService(this.apiService);
this.organizationExportService = new OrganizationVaultExportService( this.organizationExportService = new OrganizationVaultExportService(
this.cipherService, this.cipherService,
this.apiService, this.vaultExportApiService,
this.pinService, this.pinService,
this.keyService, this.keyService,
this.encryptService, this.encryptService,

View File

@@ -343,6 +343,8 @@ import { PasswordRepromptService } from "@bitwarden/vault";
import { import {
IndividualVaultExportService, IndividualVaultExportService,
IndividualVaultExportServiceAbstraction, IndividualVaultExportServiceAbstraction,
DefaultVaultExportApiService,
VaultExportApiService,
OrganizationVaultExportService, OrganizationVaultExportService,
OrganizationVaultExportServiceAbstraction, OrganizationVaultExportServiceAbstraction,
VaultExportService, VaultExportService,
@@ -887,12 +889,17 @@ const safeProviders: SafeProvider[] = [
RestrictedItemTypesService, RestrictedItemTypesService,
], ],
}), }),
safeProvider({
provide: VaultExportApiService,
useClass: DefaultVaultExportApiService,
deps: [ApiServiceAbstraction],
}),
safeProvider({ safeProvider({
provide: OrganizationVaultExportServiceAbstraction, provide: OrganizationVaultExportServiceAbstraction,
useClass: OrganizationVaultExportService, useClass: OrganizationVaultExportService,
deps: [ deps: [
CipherServiceAbstraction, CipherServiceAbstraction,
ApiServiceAbstraction, VaultExportApiService,
PinServiceAbstraction, PinServiceAbstraction,
KeyService, KeyService,
EncryptService, EncryptService,

View File

@@ -24,7 +24,6 @@ import {
OrganizationConnectionConfigApis, OrganizationConnectionConfigApis,
OrganizationConnectionResponse, OrganizationConnectionResponse,
} from "../admin-console/models/response/organization-connection.response"; } 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 { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response"; import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
import { import {
@@ -552,5 +551,4 @@ export abstract class ApiService {
request: KeyConnectorUserKeyRequest, request: KeyConnectorUserKeyRequest,
): Promise<void>; ): Promise<void>;
abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise<void>; abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise<void>;
abstract getOrganizationExport(organizationId: string): Promise<OrganizationExportResponse>;
} }

View File

@@ -33,7 +33,6 @@ import {
OrganizationConnectionConfigApis, OrganizationConnectionConfigApis,
OrganizationConnectionResponse, OrganizationConnectionResponse,
} from "../admin-console/models/response/organization-connection.response"; } 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 { OrganizationSponsorshipSyncStatusResponse } from "../admin-console/models/response/organization-sponsorship-sync-status.response";
import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response"; import { PreValidateSponsorshipResponse } from "../admin-console/models/response/pre-validate-sponsorship.response";
import { import {
@@ -1528,17 +1527,6 @@ export class ApiService implements ApiServiceAbstraction {
} }
} }
async getOrganizationExport(organizationId: string): Promise<OrganizationExportResponse> {
const r = await this.send(
"GET",
"/organizations/" + organizationId + "/export",
null,
true,
true,
);
return new OrganizationExportResponse(r);
}
// Helpers // Helpers
async getActiveBearerToken(userId: UserId): Promise<string> { async getActiveBearerToken(userId: UserId): Promise<string> {

View File

@@ -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.abstraction";
export * from "./services/individual-vault-export.service"; export * from "./services/individual-vault-export.service";
export * from "./services/export-helper"; export * from "./services/export-helper";
export * from "./services/api";

View File

@@ -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<OrganizationExportResponse> {
const r = await this.apiService.send(
"GET",
"/organizations/" + organizationId + "/export",
undefined,
true,
true,
);
return new OrganizationExportResponse(r);
}
}

View File

@@ -0,0 +1,2 @@
export * from "./vault-export-api.service.abstraction";
export * from "./default-vault-export-api.service";

View File

@@ -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<OrganizationExportResponse>;
}

View File

@@ -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<ApiService>;
let sut: VaultExportApiService;
beforeEach(() => {
apiServiceMock = mock<ApiService>();
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,
);
});
});

View File

@@ -10,7 +10,6 @@ import {
CollectionDetailsResponse, CollectionDetailsResponse,
CollectionView, CollectionView,
} from "@bitwarden/admin-console/common"; } from "@bitwarden/admin-console/common";
import { ApiService } from "@bitwarden/common/abstractions/api.service";
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service"; import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
import { getUserId } from "@bitwarden/common/auth/services/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";
@@ -34,6 +33,7 @@ import {
ExportedVaultAsString, ExportedVaultAsString,
} from "../types"; } from "../types";
import { VaultExportApiService } from "./api/vault-export-api.service.abstraction";
import { BaseVaultExportService } from "./base-vault-export.service"; import { BaseVaultExportService } from "./base-vault-export.service";
import { ExportHelper } from "./export-helper"; import { ExportHelper } from "./export-helper";
import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction"; import { OrganizationVaultExportServiceAbstraction } from "./org-vault-export.service.abstraction";
@@ -45,7 +45,7 @@ export class OrganizationVaultExportService
{ {
constructor( constructor(
private cipherService: CipherService, private cipherService: CipherService,
private apiService: ApiService, private vaultExportApiService: VaultExportApiService,
pinService: PinServiceAbstraction, pinService: PinServiceAbstraction,
private keyService: KeyService, private keyService: KeyService,
encryptService: EncryptService, encryptService: EncryptService,
@@ -138,8 +138,10 @@ export class OrganizationVaultExportService
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
promises.push( promises.push(
this.apiService.getOrganizationExport(organizationId).then((exportData) => { this.vaultExportApiService
const exportPromises: any = []; .getOrganizationExport(organizationId as OrganizationId)
.then((exportData) => {
const exportPromises: Promise<void>[] = [];
if (exportData != null) { if (exportData != null) {
if (exportData.collections != null && exportData.collections.length > 0) { if (exportData.collections != null && exportData.collections.length > 0) {
exportData.collections.forEach((c) => { exportData.collections.forEach((c) => {
@@ -149,7 +151,10 @@ export class OrganizationVaultExportService
exportPromises.push( exportPromises.push(
firstValueFrom(this.keyService.activeUserOrgKeys$) firstValueFrom(this.keyService.activeUserOrgKeys$)
.then((keys) => .then((keys) =>
collection.decrypt(keys[organizationId as OrganizationId], this.encryptService), collection.decrypt(
keys[organizationId as OrganizationId],
this.encryptService,
),
) )
.then((decCol) => { .then((decCol) => {
decCollections.push(decCol); decCollections.push(decCol);
@@ -188,39 +193,37 @@ export class OrganizationVaultExportService
private async getOrganizationEncryptedExport(organizationId: string): Promise<string> { private async getOrganizationEncryptedExport(organizationId: string): Promise<string> {
const collections: Collection[] = []; const collections: Collection[] = [];
let ciphers: Cipher[] = []; const ciphers: Cipher[] = [];
const promises = [];
promises.push( const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$);
this.apiService.getCollections(organizationId).then((c) => {
if (c != null && c.data != null && c.data.length > 0) { const exportData = await this.vaultExportApiService.getOrganizationExport(
c.data.forEach((r) => { organizationId as OrganizationId,
);
if (exportData == null) {
return;
}
if (exportData.collections != null && exportData.collections.length > 0) {
exportData.collections.forEach((c) => {
const collection = Collection.fromCollectionData( const collection = Collection.fromCollectionData(
new CollectionData(r as CollectionDetailsResponse), new CollectionData(c as CollectionDetailsResponse),
); );
collections.push(collection); collections.push(collection);
}); });
} }
}),
);
const restrictions = await firstValueFrom(this.restrictedItemTypesService.restricted$); if (exportData.ciphers != null && exportData.ciphers.length > 0) {
exportData.ciphers
promises.push( .filter((c) => c.deletedDate === null)
this.apiService.getCiphersOrganization(organizationId).then((c) => { .forEach((c) => {
if (c != null && c.data != null && c.data.length > 0) { const cipher = new Cipher(new CipherData(c));
ciphers = c.data if (!this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions)) {
.filter((item) => item.deletedDate === null) ciphers.push(cipher);
.map((item) => new Cipher(new CipherData(item))) }
.filter( });
(cipher) => !this.restrictedItemTypesService.isCipherRestricted(cipher, restrictions),
);
} }
}),
);
await Promise.all(promises);
return this.BuildEncryptedExport(organizationId, collections, ciphers); return this.BuildEncryptedExport(organizationId, collections, ciphers);
} }