1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-10 13:23:34 +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,
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,

View File

@@ -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,

View File

@@ -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,

View File

@@ -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<void>;
abstract getKeyConnectorAlive(keyConnectorUrl: string): Promise<void>;
abstract getOrganizationExport(organizationId: string): Promise<OrganizationExportResponse>;
}

View File

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