mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 13:23:34 +00:00
[PM-10749] [BEEEP] New export format: Zip with attachments (#10465)
* Add new export format: zip * Restrict zip export to just individual vaults * Add tests * Remove unused import * Fix build error * Fix tests * Fix test * Fix retrieval of ciphers by passing in activeUserId * Guard feature behind `export-attachments`-feature-flag * Extend cipher filter to also filter out any ciphers that are assigned to an organization * Added apiService to retrieve AttachmentData (metaData) and then download the attachment - Added ApiService as a depdency within DI for VaultExportService/IndividualVaultExportService - Added unit tests for filtering ciphers - Added unit test for downloading attachment metadata and attachments * Moved attachment decryption into a separate method and added unit tests * Added null check for creating the base attachment folder * Move format check for zip within Org export into an early return/throw * Add feature flag guard on the CLI * Extend ExportScopeCallout to display an individual export will contain attachment when zip-format is selected * Fix adding/removing the zip-export option based on selected vault and state of `export-attachments` feature-flag * Separate AAA visually using whitespace within tests * Remove unused error var * Write test that verifies different http request failures when retrieving attachment data * Remove uneeded ignore lint rule * Rewrite test to actually check that ciphers assigned to an org are filtered out * Introduce ExportedVault return type (#13842) * Define ExportedVault type unioned by 2 new types that describe a plain-text export vs a blob-based zip-export * Extend static getFileName to handle formats and add unit-tests * Introduce new export return type throughout the vault export module - Update abstractions - Update return types within implementations - Update callers/consumers to handle the new return value - Fix all unit tests * Add support for new export return type and fix download of blobs via CLI * Add documentation to public methods --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, Input, OnInit } from "@angular/core";
|
||||
import { Component, effect, input } from "@angular/core";
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
@@ -19,7 +19,7 @@ import { CalloutModule } from "@bitwarden/components";
|
||||
standalone: true,
|
||||
imports: [CommonModule, JslibModule, CalloutModule],
|
||||
})
|
||||
export class ExportScopeCalloutComponent implements OnInit {
|
||||
export class ExportScopeCalloutComponent {
|
||||
show = false;
|
||||
scopeConfig: {
|
||||
title: string;
|
||||
@@ -27,35 +27,23 @@ export class ExportScopeCalloutComponent implements OnInit {
|
||||
scopeIdentifier: string;
|
||||
};
|
||||
|
||||
private _organizationId: string;
|
||||
|
||||
get organizationId(): string {
|
||||
return this._organizationId;
|
||||
}
|
||||
|
||||
@Input() set organizationId(value: string) {
|
||||
this._organizationId = value;
|
||||
// FIXME: Verify that this floating promise is intentional. If it is, add an explanatory comment and ensure there is proper error handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
this.getScopeMessage(this._organizationId);
|
||||
}
|
||||
/* Optional OrganizationId, if not provided, it will display individual vault export message */
|
||||
readonly organizationId = input<string>();
|
||||
/* Optional export format, determines which individual export description to display */
|
||||
readonly exportFormat = input<string>();
|
||||
|
||||
constructor(
|
||||
protected organizationService: OrganizationService,
|
||||
protected accountService: AccountService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
if (!(await firstValueFrom(this.organizationService.hasOrganizations(userId)))) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.getScopeMessage(this.organizationId);
|
||||
this.show = true;
|
||||
) {
|
||||
effect(async () => {
|
||||
this.show = false;
|
||||
await this.getScopeMessage(this.organizationId(), this.exportFormat());
|
||||
this.show = true;
|
||||
});
|
||||
}
|
||||
|
||||
private async getScopeMessage(organizationId: string) {
|
||||
private async getScopeMessage(organizationId: string, exportFormat: string): Promise<void> {
|
||||
const userId = await firstValueFrom(getUserId(this.accountService.activeAccount$));
|
||||
this.scopeConfig =
|
||||
organizationId != null
|
||||
@@ -72,7 +60,10 @@ export class ExportScopeCalloutComponent implements OnInit {
|
||||
}
|
||||
: {
|
||||
title: "exportingPersonalVaultTitle",
|
||||
description: "exportingIndividualVaultDescription",
|
||||
description:
|
||||
exportFormat == "zip"
|
||||
? "exportingIndividualVaultWithAttachmentsDescription"
|
||||
: "exportingIndividualVaultDescription",
|
||||
scopeIdentifier: await firstValueFrom(
|
||||
this.accountService.activeAccount$.pipe(map((a) => a?.email)),
|
||||
),
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
>
|
||||
{{ "personalVaultExportPolicyInEffect" | i18n }}
|
||||
</bit-callout>
|
||||
<tools-export-scope-callout [organizationId]="organizationId"></tools-export-scope-callout>
|
||||
<tools-export-scope-callout
|
||||
[organizationId]="organizationId"
|
||||
[exportFormat]="format"
|
||||
></tools-export-scope-callout>
|
||||
|
||||
<form [formGroup]="exportForm" [bitSubmit]="submit" id="export_form_exportForm">
|
||||
<ng-container *ngIf="organizations$ | async as organizations">
|
||||
|
||||
@@ -40,6 +40,8 @@ import { Organization } from "@bitwarden/common/admin-console/models/domain/orga
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { EventType } from "@bitwarden/common/enums";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { FileDownloadService } from "@bitwarden/common/platform/abstractions/file-download/file-download.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
@@ -59,7 +61,7 @@ import {
|
||||
} from "@bitwarden/components";
|
||||
import { GeneratorServicesModule } from "@bitwarden/generator-components";
|
||||
import { CredentialGeneratorService, GenerateRequest, Generators } from "@bitwarden/generator-core";
|
||||
import { VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
||||
import { ExportedVault, VaultExportServiceAbstraction } from "@bitwarden/vault-export-core";
|
||||
|
||||
import { EncryptedExportType } from "../enums/encrypted-export-type.enum";
|
||||
|
||||
@@ -183,6 +185,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
private onlyManagedCollections = true;
|
||||
private onGenerate$ = new Subject<GenerateRequest>();
|
||||
|
||||
private isExportAttachmentsEnabled$ = this.configService.getFeatureFlag$(
|
||||
FeatureFlag.ExportAttachments,
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
protected toastService: ToastService,
|
||||
@@ -197,6 +203,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected organizationService: OrganizationService,
|
||||
private accountService: AccountService,
|
||||
private collectionService: CollectionService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
@@ -305,10 +312,20 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
)
|
||||
.subscribe();
|
||||
|
||||
this.exportForm.controls.vaultSelector.valueChanges
|
||||
combineLatest([
|
||||
this.exportForm.controls.vaultSelector.valueChanges,
|
||||
this.isExportAttachmentsEnabled$,
|
||||
])
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((value) => {
|
||||
this.organizationId = value != "myVault" ? value : undefined;
|
||||
.subscribe(([value, isExportAttachmentsEnabled]) => {
|
||||
this.organizationId = value !== "myVault" ? value : undefined;
|
||||
if (value === "myVault" && isExportAttachmentsEnabled) {
|
||||
if (!this.formatOptions.some((option) => option.value === "zip")) {
|
||||
this.formatOptions.push({ name: ".zip (with attachments)", value: "zip" });
|
||||
}
|
||||
} else {
|
||||
this.formatOptions = this.formatOptions.filter((option) => option.value !== "zip");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -344,7 +361,10 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected async doExport() {
|
||||
try {
|
||||
const data = await this.getExportData();
|
||||
|
||||
// Download the export file
|
||||
this.downloadFile(data);
|
||||
|
||||
this.toastService.showToast({
|
||||
variant: "success",
|
||||
title: null,
|
||||
@@ -429,7 +449,7 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected async getExportData(): Promise<string> {
|
||||
protected async getExportData(): Promise<ExportedVault> {
|
||||
return Utils.isNullOrWhitespace(this.organizationId)
|
||||
? this.exportService.getExport(this.format, this.filePassword)
|
||||
: this.exportService.getOrganizationExport(
|
||||
@@ -440,23 +460,6 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
);
|
||||
}
|
||||
|
||||
protected getFileName(prefix?: string) {
|
||||
if (this.organizationId) {
|
||||
prefix = "org";
|
||||
}
|
||||
|
||||
let extension = this.format;
|
||||
if (this.format === "encrypted_json") {
|
||||
if (prefix == null) {
|
||||
prefix = "encrypted";
|
||||
} else {
|
||||
prefix = "encrypted_" + prefix;
|
||||
}
|
||||
extension = "json";
|
||||
}
|
||||
return this.exportService.getFileName(prefix, extension);
|
||||
}
|
||||
|
||||
protected async collectEvent(): Promise<void> {
|
||||
if (this.organizationId) {
|
||||
return await this.eventCollectionService.collect(
|
||||
@@ -498,12 +501,11 @@ export class ExportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
}
|
||||
|
||||
private downloadFile(csv: string): void {
|
||||
const fileName = this.getFileName();
|
||||
private downloadFile(exportedVault: ExportedVault): void {
|
||||
this.fileDownloadService.download({
|
||||
fileName: fileName,
|
||||
blobData: csv,
|
||||
blobOptions: { type: "text/plain" },
|
||||
fileName: exportedVault.fileName,
|
||||
blobData: exportedVault.data,
|
||||
blobOptions: { type: exportedVault.type },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user