mirror of
https://github.com/bitwarden/browser.git
synced 2024-11-14 10:26:19 +01:00
[PM-1071] Display import-details-dialog on successful import (#4817)
* Prefer callback over error-flow to prompt for password Remove error-flow to request file password Prefer callback, which has to be provided when retrieving/creating an instance. Delete ImportError Call BitwardenPasswordProtector for all Bitwarden json imports, as it extends BitwardenJsonImporter Throw errors instead of returning Return ImportResult Fix and extend tests import.service Replace "@fluffy-spoon/substitute" with "jest-mock-extended" * Fix up test cases Delete bitwarden-json-importer.spec.ts Add test case to ensure bitwarden-json-importer.ts is called given unencrypted or account-protected files * Move file-password-prompt into dialog-folder * Add import success dialog * Fix typo * Only list the type when at least one got imported * update copy based on design feedback * Remove unnecessary /index import * Remove promptForPassword_callback from interface PR feedback from @MGibson1 that giving every importer the ability to request a password is unnecessary. Instead, we can pass the callback into the constructor for every importer that needs this functionality * Remove unneeded import of BitwardenJsonImporter * Fix spec constructor * Fixed organizational import Added an else statement, or else we'd import into an org and then also import into an individual vault
This commit is contained in:
parent
19626a7837
commit
cf2d8b266a
@ -2,7 +2,7 @@ import * as program from "commander";
|
|||||||
import * as inquirer from "inquirer";
|
import * as inquirer from "inquirer";
|
||||||
|
|
||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { ImportServiceAbstraction, Importer, ImportType } from "@bitwarden/importer";
|
import { ImportServiceAbstraction, ImportType } from "@bitwarden/importer";
|
||||||
|
|
||||||
import { Response } from "../models/response";
|
import { Response } from "../models/response";
|
||||||
import { MessageResponse } from "../models/response/message.response";
|
import { MessageResponse } from "../models/response/message.response";
|
||||||
@ -50,8 +50,14 @@ export class ImportCommand {
|
|||||||
if (filepath == null || filepath === "") {
|
if (filepath == null || filepath === "") {
|
||||||
return Response.badRequest("`filepath` was not provided.");
|
return Response.badRequest("`filepath` was not provided.");
|
||||||
}
|
}
|
||||||
|
const promptForPassword_callback = async () => {
|
||||||
const importer = await this.importService.getImporter(format, organizationId);
|
return await this.promptPassword();
|
||||||
|
};
|
||||||
|
const importer = await this.importService.getImporter(
|
||||||
|
format,
|
||||||
|
promptForPassword_callback,
|
||||||
|
organizationId
|
||||||
|
);
|
||||||
if (importer === null) {
|
if (importer === null) {
|
||||||
return Response.badRequest("Proper importer type required.");
|
return Response.badRequest("Proper importer type required.");
|
||||||
}
|
}
|
||||||
@ -68,12 +74,14 @@ export class ImportCommand {
|
|||||||
return Response.badRequest("Import file was empty.");
|
return Response.badRequest("Import file was empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.doImport(importer, contents, organizationId);
|
const response = await this.importService.import(importer, contents, organizationId);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
response.data = new MessageResponse("Imported " + filepath, null);
|
return Response.success(new MessageResponse("Imported " + filepath, null));
|
||||||
}
|
}
|
||||||
return response;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err.message) {
|
||||||
|
return Response.badRequest(err.message);
|
||||||
|
}
|
||||||
return Response.badRequest(err);
|
return Response.badRequest(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -91,27 +99,6 @@ export class ImportCommand {
|
|||||||
return Response.success(res);
|
return Response.success(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async doImport(
|
|
||||||
importer: Importer,
|
|
||||||
contents: string,
|
|
||||||
organizationId?: string
|
|
||||||
): Promise<Response> {
|
|
||||||
const err = await this.importService.import(importer, contents, organizationId);
|
|
||||||
if (err != null) {
|
|
||||||
if (err.passwordRequired) {
|
|
||||||
importer = this.importService.getImporter(
|
|
||||||
"bitwardenpasswordprotected",
|
|
||||||
organizationId,
|
|
||||||
await this.promptPassword()
|
|
||||||
);
|
|
||||||
return this.doImport(importer, contents, organizationId);
|
|
||||||
}
|
|
||||||
return Response.badRequest(err.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response.success();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async promptPassword() {
|
private async promptPassword() {
|
||||||
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
const answer: inquirer.Answers = await inquirer.createPromptModule({
|
||||||
output: process.stderr,
|
output: process.stderr,
|
||||||
|
@ -8,6 +8,7 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
|||||||
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
import { OrganizationService } from "@bitwarden/common/admin-console/abstractions/organization/organization.service.abstraction";
|
||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
import { ImportServiceAbstraction } from "@bitwarden/importer";
|
import { ImportServiceAbstraction } from "@bitwarden/importer";
|
||||||
|
|
||||||
import { ImportComponent } from "../../../../tools/import-export/import.component";
|
import { ImportComponent } from "../../../../tools/import-export/import.component";
|
||||||
@ -30,7 +31,8 @@ export class OrganizationImportComponent extends ImportComponent {
|
|||||||
private organizationService: OrganizationService,
|
private organizationService: OrganizationService,
|
||||||
logService: LogService,
|
logService: LogService,
|
||||||
modalService: ModalService,
|
modalService: ModalService,
|
||||||
syncService: SyncService
|
syncService: SyncService,
|
||||||
|
dialogService: DialogService
|
||||||
) {
|
) {
|
||||||
super(
|
super(
|
||||||
i18nService,
|
i18nService,
|
||||||
@ -40,7 +42,8 @@ export class OrganizationImportComponent extends ImportComponent {
|
|||||||
policyService,
|
policyService,
|
||||||
logService,
|
logService,
|
||||||
modalService,
|
modalService,
|
||||||
syncService
|
syncService,
|
||||||
|
dialogService
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,32 @@
|
|||||||
|
<bit-dialog dialogSize="small">
|
||||||
|
<span bitDialogTitle>
|
||||||
|
{{ "importSuccess" | i18n }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div bitDialogContent>
|
||||||
|
<span>{{ "importSuccessNumberOfItems" | i18n : this.data.ciphers.length }}</span>
|
||||||
|
<bit-table [dataSource]="dataSource">
|
||||||
|
<ng-container header>
|
||||||
|
<tr>
|
||||||
|
<th bitCell>{{ "type" | i18n }}</th>
|
||||||
|
<th bitCell>{{ "total" | i18n }}</th>
|
||||||
|
</tr>
|
||||||
|
</ng-container>
|
||||||
|
<ng-template body let-rows$>
|
||||||
|
<tr bitRow *ngFor="let r of rows$ | async">
|
||||||
|
<td bitCell>
|
||||||
|
<i class="bwi bwi-fw bwi-{{ r.icon }}" aria-hidden="true"></i>
|
||||||
|
{{ r.type | i18n }}
|
||||||
|
</td>
|
||||||
|
<td bitCell>{{ r.count }}</td>
|
||||||
|
</tr>
|
||||||
|
</ng-template>
|
||||||
|
</bit-table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div bitDialogFooter>
|
||||||
|
<button bitButton bitDialogClose buttonType="primary" type="button">
|
||||||
|
{{ "ok" | i18n }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</bit-dialog>
|
@ -0,0 +1,78 @@
|
|||||||
|
import { DialogRef, DIALOG_DATA } from "@angular/cdk/dialog";
|
||||||
|
import { Component, Inject, OnInit } from "@angular/core";
|
||||||
|
|
||||||
|
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
|
||||||
|
import { TableDataSource } from "@bitwarden/components";
|
||||||
|
import { ImportResult } from "@bitwarden/importer";
|
||||||
|
|
||||||
|
export interface ResultList {
|
||||||
|
icon: string;
|
||||||
|
type: string;
|
||||||
|
count: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: "app-import-success-dialog",
|
||||||
|
templateUrl: "./import-success-dialog.component.html",
|
||||||
|
})
|
||||||
|
export class ImportSuccessDialogComponent implements OnInit {
|
||||||
|
protected dataSource = new TableDataSource<ResultList>();
|
||||||
|
|
||||||
|
constructor(public dialogRef: DialogRef, @Inject(DIALOG_DATA) public data: ImportResult) {}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
if (this.data != null) {
|
||||||
|
this.dataSource.data = this.buildResultList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildResultList(): ResultList[] {
|
||||||
|
let logins = 0;
|
||||||
|
let cards = 0;
|
||||||
|
let identities = 0;
|
||||||
|
let secureNotes = 0;
|
||||||
|
this.data.ciphers.map((c) => {
|
||||||
|
switch (c.type) {
|
||||||
|
case CipherType.Login:
|
||||||
|
logins++;
|
||||||
|
break;
|
||||||
|
case CipherType.Card:
|
||||||
|
cards++;
|
||||||
|
break;
|
||||||
|
case CipherType.SecureNote:
|
||||||
|
secureNotes++;
|
||||||
|
break;
|
||||||
|
case CipherType.Identity:
|
||||||
|
identities++;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const list: ResultList[] = [];
|
||||||
|
if (logins > 0) {
|
||||||
|
list.push({ icon: "globe", type: "typeLogin", count: logins });
|
||||||
|
}
|
||||||
|
if (cards > 0) {
|
||||||
|
list.push({ icon: "credit-card", type: "typeCard", count: cards });
|
||||||
|
}
|
||||||
|
if (identities > 0) {
|
||||||
|
list.push({ icon: "id-card", type: "typeIdentity", count: identities });
|
||||||
|
}
|
||||||
|
if (secureNotes > 0) {
|
||||||
|
list.push({ icon: "sticky-note", type: "typeSecureNote", count: secureNotes });
|
||||||
|
}
|
||||||
|
if (this.data.folders.length > 0) {
|
||||||
|
list.push({ icon: "folder", type: "folders", count: this.data.folders.length });
|
||||||
|
}
|
||||||
|
if (this.data.collections.length > 0) {
|
||||||
|
list.push({
|
||||||
|
icon: "collection",
|
||||||
|
type: "collections",
|
||||||
|
count: this.data.collections.length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
}
|
2
apps/web/src/app/tools/import-export/dialog/index.ts
Normal file
2
apps/web/src/app/tools/import-export/dialog/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from "./import-success-dialog.component";
|
||||||
|
export * from "./file-password-prompt.component";
|
@ -15,14 +15,19 @@ import {
|
|||||||
|
|
||||||
import { LooseComponentsModule, SharedModule } from "../../shared";
|
import { LooseComponentsModule, SharedModule } from "../../shared";
|
||||||
|
|
||||||
|
import { ImportSuccessDialogComponent, FilePasswordPromptComponent } from "./dialog";
|
||||||
import { ExportComponent } from "./export.component";
|
import { ExportComponent } from "./export.component";
|
||||||
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
|
|
||||||
import { ImportExportRoutingModule } from "./import-export-routing.module";
|
import { ImportExportRoutingModule } from "./import-export-routing.module";
|
||||||
import { ImportComponent } from "./import.component";
|
import { ImportComponent } from "./import.component";
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [SharedModule, LooseComponentsModule, ImportExportRoutingModule],
|
imports: [SharedModule, LooseComponentsModule, ImportExportRoutingModule],
|
||||||
declarations: [ImportComponent, ExportComponent, FilePasswordPromptComponent],
|
declarations: [
|
||||||
|
ImportComponent,
|
||||||
|
ExportComponent,
|
||||||
|
FilePasswordPromptComponent,
|
||||||
|
ImportSuccessDialogComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{
|
{
|
||||||
provide: ImportApiServiceAbstraction,
|
provide: ImportApiServiceAbstraction,
|
||||||
|
@ -11,14 +11,15 @@ import { PlatformUtilsService } from "@bitwarden/common/abstractions/platformUti
|
|||||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||||
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
import { PolicyType } from "@bitwarden/common/admin-console/enums";
|
||||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||||
|
import { DialogService } from "@bitwarden/components";
|
||||||
import {
|
import {
|
||||||
ImportOption,
|
ImportOption,
|
||||||
ImportType,
|
ImportType,
|
||||||
ImportError,
|
ImportResult,
|
||||||
ImportServiceAbstraction,
|
ImportServiceAbstraction,
|
||||||
} from "@bitwarden/importer";
|
} from "@bitwarden/importer";
|
||||||
|
|
||||||
import { FilePasswordPromptComponent } from "./file-password-prompt.component";
|
import { ImportSuccessDialogComponent, FilePasswordPromptComponent } from "./dialog";
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: "app-import",
|
selector: "app-import",
|
||||||
@ -30,7 +31,6 @@ export class ImportComponent implements OnInit {
|
|||||||
format: ImportType = null;
|
format: ImportType = null;
|
||||||
fileContents: string;
|
fileContents: string;
|
||||||
fileSelected: File;
|
fileSelected: File;
|
||||||
formPromise: Promise<ImportError>;
|
|
||||||
loading = false;
|
loading = false;
|
||||||
importBlockedByPolicy = false;
|
importBlockedByPolicy = false;
|
||||||
|
|
||||||
@ -45,7 +45,8 @@ export class ImportComponent implements OnInit {
|
|||||||
protected policyService: PolicyService,
|
protected policyService: PolicyService,
|
||||||
private logService: LogService,
|
private logService: LogService,
|
||||||
protected modalService: ModalService,
|
protected modalService: ModalService,
|
||||||
protected syncService: SyncService
|
protected syncService: SyncService,
|
||||||
|
protected dialogService: DialogService
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
@ -68,7 +69,15 @@ export class ImportComponent implements OnInit {
|
|||||||
|
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
|
|
||||||
const importer = this.importService.getImporter(this.format, this.organizationId);
|
const promptForPassword_callback = async () => {
|
||||||
|
return await this.getFilePassword();
|
||||||
|
};
|
||||||
|
|
||||||
|
const importer = this.importService.getImporter(
|
||||||
|
this.format,
|
||||||
|
promptForPassword_callback,
|
||||||
|
this.organizationId
|
||||||
|
);
|
||||||
if (importer === null) {
|
if (importer === null) {
|
||||||
this.platformUtilsService.showToast(
|
this.platformUtilsService.showToast(
|
||||||
"error",
|
"error",
|
||||||
@ -117,30 +126,17 @@ export class ImportComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.formPromise = this.importService.import(importer, fileContents, this.organizationId);
|
const result = await this.importService.import(importer, fileContents, this.organizationId);
|
||||||
let error = await this.formPromise;
|
|
||||||
|
|
||||||
if (error?.passwordRequired) {
|
|
||||||
const filePassword = await this.getFilePassword();
|
|
||||||
if (filePassword == null) {
|
|
||||||
this.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
error = await this.doPasswordProtectedImport(filePassword, fileContents);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != null) {
|
|
||||||
this.error(error);
|
|
||||||
this.loading = false;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//No errors, display success message
|
//No errors, display success message
|
||||||
this.platformUtilsService.showToast("success", null, this.i18nService.t("importSuccess"));
|
this.dialogService.open<unknown, ImportResult>(ImportSuccessDialogComponent, {
|
||||||
|
data: result,
|
||||||
|
});
|
||||||
|
|
||||||
this.syncService.fullSync(true);
|
this.syncService.fullSync(true);
|
||||||
this.router.navigate(this.successNavigate);
|
this.router.navigate(this.successNavigate);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
this.error(e);
|
||||||
this.logService.error(e);
|
this.logService.error(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -268,17 +264,4 @@ export class ImportComponent implements OnInit {
|
|||||||
|
|
||||||
return await ref.onClosedPromise();
|
return await ref.onClosedPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
async doPasswordProtectedImport(
|
|
||||||
filePassword: string,
|
|
||||||
fileContents: string
|
|
||||||
): Promise<ImportError> {
|
|
||||||
const passwordProtectedImporter = this.importService.getImporter(
|
|
||||||
"bitwardenpasswordprotected",
|
|
||||||
this.organizationId,
|
|
||||||
filePassword
|
|
||||||
);
|
|
||||||
|
|
||||||
return this.importService.import(passwordProtectedImporter, fileContents, this.organizationId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1256,6 +1256,15 @@
|
|||||||
"importSuccess": {
|
"importSuccess": {
|
||||||
"message": "Data successfully imported"
|
"message": "Data successfully imported"
|
||||||
},
|
},
|
||||||
|
"importSuccessNumberOfItems": {
|
||||||
|
"message": "A total of $AMOUNT$ items were imported.",
|
||||||
|
"placeholders": {
|
||||||
|
"amount": {
|
||||||
|
"content": "$1",
|
||||||
|
"example": "2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dataExportSuccess": {
|
"dataExportSuccess": {
|
||||||
"message": "Data successfully exported"
|
"message": "Data successfully exported"
|
||||||
},
|
},
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
|
||||||
import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
|
||||||
|
|
||||||
import { BitwardenJsonImporter } from "../src/importers";
|
|
||||||
|
|
||||||
import { data as passwordProtectedData } from "./test-data/bitwarden-json/password-protected.json";
|
|
||||||
|
|
||||||
describe("bitwarden json importer", () => {
|
|
||||||
let sut: BitwardenJsonImporter;
|
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
|
||||||
let i18nService: SubstituteOf<I18nService>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
i18nService = Substitute.for<I18nService>();
|
|
||||||
|
|
||||||
sut = new BitwardenJsonImporter(cryptoService, i18nService);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should fail if password is needed", async () => {
|
|
||||||
expect((await sut.parse(passwordProtectedData)).success).toBe(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return password needed error message", async () => {
|
|
||||||
const expected = "Password required error message";
|
|
||||||
i18nService.t("importPasswordRequired").returns(expected);
|
|
||||||
|
|
||||||
expect((await sut.parse(passwordProtectedData)).errorMessage).toEqual(expected);
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,22 +1,61 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { Substitute, Arg, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
import { KdfType } from "@bitwarden/common/enums";
|
import { KdfType } from "@bitwarden/common/enums";
|
||||||
import { Utils } from "@bitwarden/common/misc/utils";
|
import { Utils } from "@bitwarden/common/misc/utils";
|
||||||
|
|
||||||
import { BitwardenPasswordProtectedImporter } from "../src/importers";
|
import {
|
||||||
import { ImportResult } from "../src/models/import-result";
|
BitwardenPasswordProtectedImporter,
|
||||||
|
BitwardenJsonImporter,
|
||||||
|
} from "../src/importers/bitwarden";
|
||||||
|
|
||||||
import { data as emptyDecryptedData } from "./test-data/bitwarden-json/empty.json";
|
import { emptyAccountEncrypted } from "./test-data/bitwarden-json/account-encrypted.json";
|
||||||
|
import { emptyUnencryptedExport } from "./test-data/bitwarden-json/unencrypted.json";
|
||||||
|
|
||||||
describe("BitwardenPasswordProtectedImporter", () => {
|
describe("BitwardenPasswordProtectedImporter", () => {
|
||||||
let importer: BitwardenPasswordProtectedImporter;
|
let importer: BitwardenPasswordProtectedImporter;
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
let i18nService: SubstituteOf<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
const password = Utils.newGuid();
|
const password = Utils.newGuid();
|
||||||
const result = new ImportResult();
|
const promptForPassword_callback = async () => {
|
||||||
|
return password;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cryptoService = mock<CryptoService>();
|
||||||
|
i18nService = mock<I18nService>();
|
||||||
|
|
||||||
|
importer = new BitwardenPasswordProtectedImporter(
|
||||||
|
cryptoService,
|
||||||
|
i18nService,
|
||||||
|
promptForPassword_callback
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Unencrypted", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(BitwardenJsonImporter.prototype, "parse");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should call BitwardenJsonImporter", async () => {
|
||||||
|
expect((await importer.parse(emptyUnencryptedExport)).success).toEqual(true);
|
||||||
|
expect(BitwardenJsonImporter.prototype.parse).toHaveBeenCalledWith(emptyUnencryptedExport);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Account encrypted", () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
jest.spyOn(BitwardenJsonImporter.prototype, "parse");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Should call BitwardenJsonImporter", async () => {
|
||||||
|
expect((await importer.parse(emptyAccountEncrypted)).success).toEqual(true);
|
||||||
|
expect(BitwardenJsonImporter.prototype.parse).toHaveBeenCalledWith(emptyAccountEncrypted);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Password protected", () => {
|
||||||
let jDoc: {
|
let jDoc: {
|
||||||
encrypted?: boolean;
|
encrypted?: boolean;
|
||||||
passwordProtected?: boolean;
|
passwordProtected?: boolean;
|
||||||
@ -28,9 +67,6 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
|
||||||
i18nService = Substitute.for<I18nService>();
|
|
||||||
|
|
||||||
jDoc = {
|
jDoc = {
|
||||||
encrypted: true,
|
encrypted: true,
|
||||||
passwordProtected: true,
|
passwordProtected: true,
|
||||||
@ -40,38 +76,14 @@ describe("BitwardenPasswordProtectedImporter", () => {
|
|||||||
encKeyValidation_DO_NOT_EDIT: Utils.newGuid(),
|
encKeyValidation_DO_NOT_EDIT: Utils.newGuid(),
|
||||||
data: Utils.newGuid(),
|
data: Utils.newGuid(),
|
||||||
};
|
};
|
||||||
|
|
||||||
result.success = true;
|
|
||||||
importer = new BitwardenPasswordProtectedImporter(cryptoService, i18nService, password);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Required Json Data", () => {
|
|
||||||
it("succeeds with default jdoc", async () => {
|
it("succeeds with default jdoc", async () => {
|
||||||
cryptoService.decryptToUtf8(Arg.any(), Arg.any()).resolves(emptyDecryptedData);
|
cryptoService.decryptToUtf8.mockReturnValue(Promise.resolve(emptyUnencryptedExport));
|
||||||
|
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true);
|
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("fails if encrypted === false", async () => {
|
|
||||||
jDoc.encrypted = false;
|
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails if encrypted === null", async () => {
|
|
||||||
jDoc.encrypted = null;
|
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails if passwordProtected === false", async () => {
|
|
||||||
jDoc.passwordProtected = false;
|
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails if passwordProtected === null", async () => {
|
|
||||||
jDoc.passwordProtected = null;
|
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("fails if salt === null", async () => {
|
it("fails if salt === null", async () => {
|
||||||
jDoc.salt = null;
|
jDoc.salt = null;
|
||||||
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
expect((await importer.parse(JSON.stringify(jDoc))).success).toEqual(false);
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
export const emptyAccountEncrypted = `{
|
||||||
|
"encrypted": true,
|
||||||
|
"encKeyValidation_DO_NOT_EDIT": "",
|
||||||
|
"folders": [],
|
||||||
|
"items": []
|
||||||
|
}`;
|
@ -1 +0,0 @@
|
|||||||
export const data = '{"encrypted":false,"folders":[],"items":[]}';
|
|
@ -1,9 +1,10 @@
|
|||||||
export const data = `{
|
export const data = {
|
||||||
"encrypted": true,
|
encrypted: true,
|
||||||
"passwordProtected": true,
|
passwordProtected: true,
|
||||||
"salt": "Oy0xcgVRzxQ+9NpB5GLehw==",
|
salt: "Oy0xcgVRzxQ+9NpB5GLehw==",
|
||||||
"kdfIterations": 100000,
|
kdfIterations: 100000,
|
||||||
"kdfType": 0,
|
kdfType: 0,
|
||||||
"encKeyValidation_DO_NOT_EDIT": "2.sZs4Jc1HW9rhABzRRYR/gQ==|8kTDaDxafulnybpWoqVX8RAybhVRTr+dffNjms271Y7amQmIE1VSMwLbk+b2vxZb|IqOo6oXQtmv/Xb/GHDi42XG9c9ILePYtP5qq584VWcg=",
|
encKeyValidation_DO_NOT_EDIT:
|
||||||
"data": "2.D0AXAf7G/XIwq6EC7A0Suw==|4w+m0wHRo25y1T1Syh5wdAUyF8voqEy54waMEsbnK0Nzee959w54ru5D1NntvxZL4HFqkQLyR6jCFkn5g40f+MGJgihS/wvf4NcJJfLiiFo6MEDOQNBkxw7ZBGuHiKfVuBO5u36JgzQtZ8lyFaduGxFszuF5c+URiE9PDh9jY0//poVgHKwuLZuYFIW+f7h6T+shUWK0ya11lcHn/B/CA2xiI+YiKdNZreJrwN0yslpJ/f+MrOzagvftRjt0GNkwveCtwcYUw/zFvqvibUpKeHcRiXs8SaGoHJ5RTm69FbJ7C5tnLwoVT89Af156uvRAXV7yAC4oPcbU/3TGb6hqYosvi1QNyaqG3M9gxS6+AK0C4yWuNbMLDEr+MWiw0SWLVMKQEkCZ4oM+oTCx52otW3+2V9I8Pv3KmmhkvVvE4wBdweOJeRX53Tf5ySkmpIhCfzj6JMmxO+nmTXIhWnJChr4hPVh+ixv1GQK5thIPTCMXmAtXoTIFUx1KWjS6LjOdi2hKQueVI+XZjf0qnY2vTMxRg0ZsLBA2znQTx+DSEqumORb5T/lV73pWZiCNePSAE2msOm7tep+lm4O/VCViCfXjITAY196syhOK0XnhxJvPALchZY8sYRAfuw6hHoDiVr+JUieRoI7eUrhXBp+D6Py9TL/dS/rHe+C2Zhx+xwx2NfGt+xEp8ZAOOCxgZ0UTeSA/abm0Oz7tJIK1n26acQrgbr7rMeBymAX+5L5OWlwI1hGgEBfj6W0rrbSXf3VMfaFXZ5UsXi1VhzQmU3LyWENoDeImXFQj6zMbUSfcVwLsG5Fg8Ee/kO/wJPfG5BO51+/vFqQj6AkaMEcwg5xNrObHYfQ/DMhIn7YDM2zdzbNTdhnobGkz6YRKFPCgFe3EmIEPEpeh9S3eKE9C7MQsrR8jVSiseR/FipJLsN+W7iOwzeXdwxUFlC/0a98bTKvdrbMgNi6ZVXykHY/t2UyEGpxZGTHoZwhX01kiQrwzC4/+v/676ldxPluO9GY7MtrLveCDsiyBz15u43IGHayDEBNT0rqrOKLYmfzwCWoahRLZQrSmepe/FXqgPqRfyWc/Ro+w3sT9dXUkx3B5xxWgSyABowPV48yBUSJuefhKTpqgzkU+LzhNnWHjnxJzzQ2/|IhlRjnyhIoDM85qHX/bY2zaIU5YaRO/iFVTQDd3uFDo="
|
"2.sZs4Jc1HW9rhABzRRYR/gQ==|8kTDaDxafulnybpWoqVX8RAybhVRTr+dffNjms271Y7amQmIE1VSMwLbk+b2vxZb|IqOo6oXQtmv/Xb/GHDi42XG9c9ILePYtP5qq584VWcg=",
|
||||||
}`;
|
data: "2.D0AXAf7G/XIwq6EC7A0Suw==|4w+m0wHRo25y1T1Syh5wdAUyF8voqEy54waMEsbnK0Nzee959w54ru5D1NntvxZL4HFqkQLyR6jCFkn5g40f+MGJgihS/wvf4NcJJfLiiFo6MEDOQNBkxw7ZBGuHiKfVuBO5u36JgzQtZ8lyFaduGxFszuF5c+URiE9PDh9jY0//poVgHKwuLZuYFIW+f7h6T+shUWK0ya11lcHn/B/CA2xiI+YiKdNZreJrwN0yslpJ/f+MrOzagvftRjt0GNkwveCtwcYUw/zFvqvibUpKeHcRiXs8SaGoHJ5RTm69FbJ7C5tnLwoVT89Af156uvRAXV7yAC4oPcbU/3TGb6hqYosvi1QNyaqG3M9gxS6+AK0C4yWuNbMLDEr+MWiw0SWLVMKQEkCZ4oM+oTCx52otW3+2V9I8Pv3KmmhkvVvE4wBdweOJeRX53Tf5ySkmpIhCfzj6JMmxO+nmTXIhWnJChr4hPVh+ixv1GQK5thIPTCMXmAtXoTIFUx1KWjS6LjOdi2hKQueVI+XZjf0qnY2vTMxRg0ZsLBA2znQTx+DSEqumORb5T/lV73pWZiCNePSAE2msOm7tep+lm4O/VCViCfXjITAY196syhOK0XnhxJvPALchZY8sYRAfuw6hHoDiVr+JUieRoI7eUrhXBp+D6Py9TL/dS/rHe+C2Zhx+xwx2NfGt+xEp8ZAOOCxgZ0UTeSA/abm0Oz7tJIK1n26acQrgbr7rMeBymAX+5L5OWlwI1hGgEBfj6W0rrbSXf3VMfaFXZ5UsXi1VhzQmU3LyWENoDeImXFQj6zMbUSfcVwLsG5Fg8Ee/kO/wJPfG5BO51+/vFqQj6AkaMEcwg5xNrObHYfQ/DMhIn7YDM2zdzbNTdhnobGkz6YRKFPCgFe3EmIEPEpeh9S3eKE9C7MQsrR8jVSiseR/FipJLsN+W7iOwzeXdwxUFlC/0a98bTKvdrbMgNi6ZVXykHY/t2UyEGpxZGTHoZwhX01kiQrwzC4/+v/676ldxPluO9GY7MtrLveCDsiyBz15u43IGHayDEBNT0rqrOKLYmfzwCWoahRLZQrSmepe/FXqgPqRfyWc/Ro+w3sT9dXUkx3B5xxWgSyABowPV48yBUSJuefhKTpqgzkU+LzhNnWHjnxJzzQ2/|IhlRjnyhIoDM85qHX/bY2zaIU5YaRO/iFVTQDd3uFDo=",
|
||||||
|
};
|
||||||
|
@ -0,0 +1 @@
|
|||||||
|
export const emptyUnencryptedExport = `{ "encrypted": false, "folders": [], "items": [] }`;
|
@ -13,7 +13,10 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
|||||||
private results: any;
|
private results: any;
|
||||||
private result: ImportResult;
|
private result: ImportResult;
|
||||||
|
|
||||||
constructor(protected cryptoService: CryptoService, protected i18nService: I18nService) {
|
protected constructor(
|
||||||
|
protected cryptoService: CryptoService,
|
||||||
|
protected i18nService: I18nService
|
||||||
|
) {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -21,13 +24,6 @@ export class BitwardenJsonImporter extends BaseImporter implements Importer {
|
|||||||
this.result = new ImportResult();
|
this.result = new ImportResult();
|
||||||
this.results = JSON.parse(data);
|
this.results = JSON.parse(data);
|
||||||
if (this.results == null || this.results.items == null) {
|
if (this.results == null || this.results.items == null) {
|
||||||
if (this.results?.passwordProtected) {
|
|
||||||
this.result.success = false;
|
|
||||||
this.result.missingPassword = true;
|
|
||||||
this.result.errorMessage = this.i18nService.t("importPasswordRequired");
|
|
||||||
return this.result;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.result.success = false;
|
this.result.success = false;
|
||||||
return this.result;
|
return this.result;
|
||||||
}
|
}
|
||||||
|
@ -13,19 +13,41 @@ import { BitwardenPasswordProtectedFileFormat } from "./bitwarden-password-prote
|
|||||||
export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer {
|
export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter implements Importer {
|
||||||
private key: SymmetricCryptoKey;
|
private key: SymmetricCryptoKey;
|
||||||
|
|
||||||
constructor(cryptoService: CryptoService, i18nService: I18nService, private password: string) {
|
constructor(
|
||||||
|
cryptoService: CryptoService,
|
||||||
|
i18nService: I18nService,
|
||||||
|
private promptForPassword_callback: () => Promise<string>
|
||||||
|
) {
|
||||||
super(cryptoService, i18nService);
|
super(cryptoService, i18nService);
|
||||||
}
|
}
|
||||||
|
|
||||||
async parse(data: string): Promise<ImportResult> {
|
async parse(data: string): Promise<ImportResult> {
|
||||||
const result = new ImportResult();
|
const result = new ImportResult();
|
||||||
const parsedData = JSON.parse(data);
|
const parsedData: BitwardenPasswordProtectedFileFormat = JSON.parse(data);
|
||||||
|
|
||||||
|
if (!parsedData) {
|
||||||
|
result.success = false;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is unencrypted
|
||||||
|
if (!parsedData?.encrypted) {
|
||||||
|
return await super.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// File is account-encrypted
|
||||||
|
if (!parsedData?.passwordProtected) {
|
||||||
|
return await super.parse(data);
|
||||||
|
}
|
||||||
|
|
||||||
if (this.cannotParseFile(parsedData)) {
|
if (this.cannotParseFile(parsedData)) {
|
||||||
result.success = false;
|
result.success = false;
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await this.checkPassword(parsedData))) {
|
// File is password-protected
|
||||||
|
const password = await this.promptForPassword_callback();
|
||||||
|
if (!(await this.checkPassword(parsedData, password))) {
|
||||||
result.success = false;
|
result.success = false;
|
||||||
result.errorMessage = this.i18nService.t("invalidFilePassword");
|
result.errorMessage = this.i18nService.t("invalidFilePassword");
|
||||||
return result;
|
return result;
|
||||||
@ -36,9 +58,12 @@ export class BitwardenPasswordProtectedImporter extends BitwardenJsonImporter im
|
|||||||
return await super.parse(clearTextData);
|
return await super.parse(clearTextData);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async checkPassword(jdoc: BitwardenPasswordProtectedFileFormat): Promise<boolean> {
|
private async checkPassword(
|
||||||
|
jdoc: BitwardenPasswordProtectedFileFormat,
|
||||||
|
password: string
|
||||||
|
): Promise<boolean> {
|
||||||
this.key = await this.cryptoService.makePinKey(
|
this.key = await this.cryptoService.makePinKey(
|
||||||
this.password,
|
password,
|
||||||
jdoc.salt,
|
jdoc.salt,
|
||||||
jdoc.kdfType,
|
jdoc.kdfType,
|
||||||
new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism)
|
new KdfConfig(jdoc.kdfIterations, jdoc.kdfMemory, jdoc.kdfParallelism)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
export { ImportType, ImportOption } from "./models/import-options";
|
export { ImportType, ImportOption } from "./models/import-options";
|
||||||
|
|
||||||
export { ImportError } from "./models/import-error";
|
export { ImportResult } from "./models/import-result";
|
||||||
|
|
||||||
export { ImportApiServiceAbstraction } from "./services/import-api.service.abstraction";
|
export { ImportApiServiceAbstraction } from "./services/import-api.service.abstraction";
|
||||||
export { ImportApiService } from "./services/import-api.service";
|
export { ImportApiService } from "./services/import-api.service";
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
export class ImportError extends Error {
|
|
||||||
constructor(message?: string, public passwordRequired: boolean = false) {
|
|
||||||
super(message);
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
|||||||
|
|
||||||
export class ImportResult {
|
export class ImportResult {
|
||||||
success = false;
|
success = false;
|
||||||
missingPassword = false;
|
|
||||||
errorMessage: string;
|
errorMessage: string;
|
||||||
ciphers: CipherView[] = [];
|
ciphers: CipherView[] = [];
|
||||||
folders: FolderView[] = [];
|
folders: FolderView[] = [];
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
import { ImportError } from "../models/import-error";
|
|
||||||
import { ImportOption, ImportType } from "../models/import-options";
|
import { ImportOption, ImportType } from "../models/import-options";
|
||||||
|
import { ImportResult } from "../models/import-result";
|
||||||
|
|
||||||
export abstract class ImportServiceAbstraction {
|
export abstract class ImportServiceAbstraction {
|
||||||
featuredImportOptions: readonly ImportOption[];
|
featuredImportOptions: readonly ImportOption[];
|
||||||
@ -10,10 +10,10 @@ export abstract class ImportServiceAbstraction {
|
|||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
organizationId?: string
|
organizationId?: string
|
||||||
) => Promise<ImportError>;
|
) => Promise<ImportResult>;
|
||||||
getImporter: (
|
getImporter: (
|
||||||
format: ImportType | "bitwardenpasswordprotected",
|
format: ImportType | "bitwardenpasswordprotected",
|
||||||
organizationId: string,
|
promptForPassword_callback: () => Promise<string>,
|
||||||
password?: string
|
organizationId: string
|
||||||
) => Importer;
|
) => Importer;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
// eslint-disable-next-line no-restricted-imports
|
import { mock, MockProxy } from "jest-mock-extended";
|
||||||
import { Substitute, SubstituteOf } from "@fluffy-spoon/substitute";
|
|
||||||
|
|
||||||
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
import { CryptoService } from "@bitwarden/common/abstractions/crypto.service";
|
||||||
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/abstractions/i18n.service";
|
||||||
@ -16,20 +15,20 @@ import { ImportService } from "./import.service";
|
|||||||
|
|
||||||
describe("ImportService", () => {
|
describe("ImportService", () => {
|
||||||
let importService: ImportService;
|
let importService: ImportService;
|
||||||
let cipherService: SubstituteOf<CipherService>;
|
let cipherService: MockProxy<CipherService>;
|
||||||
let folderService: SubstituteOf<FolderService>;
|
let folderService: MockProxy<FolderService>;
|
||||||
let importApiService: SubstituteOf<ImportApiServiceAbstraction>;
|
let importApiService: MockProxy<ImportApiServiceAbstraction>;
|
||||||
let i18nService: SubstituteOf<I18nService>;
|
let i18nService: MockProxy<I18nService>;
|
||||||
let collectionService: SubstituteOf<CollectionService>;
|
let collectionService: MockProxy<CollectionService>;
|
||||||
let cryptoService: SubstituteOf<CryptoService>;
|
let cryptoService: MockProxy<CryptoService>;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cipherService = Substitute.for<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
folderService = Substitute.for<FolderService>();
|
folderService = mock<FolderService>();
|
||||||
importApiService = Substitute.for<ImportApiServiceAbstraction>();
|
importApiService = mock<ImportApiServiceAbstraction>();
|
||||||
i18nService = Substitute.for<I18nService>();
|
i18nService = mock<I18nService>();
|
||||||
collectionService = Substitute.for<CollectionService>();
|
collectionService = mock<CollectionService>();
|
||||||
cryptoService = Substitute.for<CryptoService>();
|
cryptoService = mock<CryptoService>();
|
||||||
|
|
||||||
importService = new ImportService(
|
importService = new ImportService(
|
||||||
cipherService,
|
cipherService,
|
||||||
@ -46,12 +45,15 @@ describe("ImportService", () => {
|
|||||||
let importer: Importer;
|
let importer: Importer;
|
||||||
const organizationId = Utils.newGuid();
|
const organizationId = Utils.newGuid();
|
||||||
const password = Utils.newGuid();
|
const password = Utils.newGuid();
|
||||||
|
const promptForPassword_callback = async () => {
|
||||||
|
return password;
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
importer = importService.getImporter(
|
importer = importService.getImporter(
|
||||||
"bitwardenpasswordprotected",
|
"bitwardenpasswordprotected",
|
||||||
organizationId,
|
promptForPassword_callback,
|
||||||
password
|
organizationId
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -59,13 +61,14 @@ describe("ImportService", () => {
|
|||||||
expect(importer).toBeInstanceOf(BitwardenPasswordProtectedImporter);
|
expect(importer).toBeInstanceOf(BitwardenPasswordProtectedImporter);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("has the promptForPassword_callback set", async () => {
|
||||||
|
expect(importer.promptForPassword_callback).not.toBeNull();
|
||||||
|
expect(await importer.promptForPassword_callback()).toEqual(password);
|
||||||
|
});
|
||||||
|
|
||||||
it("has the appropriate organization Id", () => {
|
it("has the appropriate organization Id", () => {
|
||||||
expect(importer.organizationId).toEqual(organizationId);
|
expect(importer.organizationId).toEqual(organizationId);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has the appropriate password", () => {
|
|
||||||
expect(Object.entries(importer)).toEqual(expect.arrayContaining([["password", password]]));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,6 @@ import {
|
|||||||
AvastJsonImporter,
|
AvastJsonImporter,
|
||||||
AviraCsvImporter,
|
AviraCsvImporter,
|
||||||
BitwardenCsvImporter,
|
BitwardenCsvImporter,
|
||||||
BitwardenJsonImporter,
|
|
||||||
BitwardenPasswordProtectedImporter,
|
BitwardenPasswordProtectedImporter,
|
||||||
BlackBerryCsvImporter,
|
BlackBerryCsvImporter,
|
||||||
BlurCsvImporter,
|
BlurCsvImporter,
|
||||||
@ -76,7 +75,6 @@ import {
|
|||||||
ZohoVaultCsvImporter,
|
ZohoVaultCsvImporter,
|
||||||
} from "../importers";
|
} from "../importers";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
import { ImportError } from "../models/import-error";
|
|
||||||
import {
|
import {
|
||||||
featuredImportOptions,
|
featuredImportOptions,
|
||||||
ImportOption,
|
ImportOption,
|
||||||
@ -109,12 +107,20 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
organizationId: string = null
|
organizationId: string = null
|
||||||
): Promise<ImportError> {
|
): Promise<ImportResult> {
|
||||||
const importResult = await importer.parse(fileContents);
|
const importResult = await importer.parse(fileContents);
|
||||||
if (importResult.success) {
|
if (!importResult.success) {
|
||||||
|
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
|
||||||
|
throw new Error(importResult.errorMessage);
|
||||||
|
}
|
||||||
|
throw new Error(this.i18nService.t("importFormatError"));
|
||||||
|
}
|
||||||
|
|
||||||
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
|
if (importResult.folders.length === 0 && importResult.ciphers.length === 0) {
|
||||||
return new ImportError(this.i18nService.t("importNothingError"));
|
throw new Error(this.i18nService.t("importNothingError"));
|
||||||
} else if (importResult.ciphers.length > 0) {
|
}
|
||||||
|
|
||||||
|
if (importResult.ciphers.length > 0) {
|
||||||
const halfway = Math.floor(importResult.ciphers.length / 2);
|
const halfway = Math.floor(importResult.ciphers.length / 2);
|
||||||
const last = importResult.ciphers.length - 1;
|
const last = importResult.ciphers.length - 1;
|
||||||
|
|
||||||
@ -123,34 +129,33 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
this.badData(importResult.ciphers[halfway]) &&
|
this.badData(importResult.ciphers[halfway]) &&
|
||||||
this.badData(importResult.ciphers[last])
|
this.badData(importResult.ciphers[last])
|
||||||
) {
|
) {
|
||||||
return new ImportError(this.i18nService.t("importFormatError"));
|
throw new Error(this.i18nService.t("importFormatError"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.postImport(importResult, organizationId);
|
if (organizationId != null) {
|
||||||
|
await this.handleOrganizationalImport(importResult, organizationId);
|
||||||
|
} else {
|
||||||
|
await this.handleIndividualImport(importResult);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorResponse = new ErrorResponse(error, 400);
|
const errorResponse = new ErrorResponse(error, 400);
|
||||||
return this.handleServerError(errorResponse, importResult);
|
throw this.handleServerError(errorResponse, importResult);
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} else {
|
|
||||||
if (!Utils.isNullOrWhitespace(importResult.errorMessage)) {
|
|
||||||
return new ImportError(importResult.errorMessage, importResult.missingPassword);
|
|
||||||
} else {
|
|
||||||
return new ImportError(
|
|
||||||
this.i18nService.t("importFormatError"),
|
|
||||||
importResult.missingPassword
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return importResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
getImporter(
|
getImporter(
|
||||||
format: ImportType | "bitwardenpasswordprotected",
|
format: ImportType | "bitwardenpasswordprotected",
|
||||||
organizationId: string = null,
|
promptForPassword_callback: () => Promise<string>,
|
||||||
password: string = null
|
organizationId: string = null
|
||||||
): Importer {
|
): Importer {
|
||||||
const importer = this.getImporterInstance(format, password);
|
if (promptForPassword_callback == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const importer = this.getImporterInstance(format, promptForPassword_callback);
|
||||||
if (importer == null) {
|
if (importer == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -158,7 +163,10 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
return importer;
|
return importer;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getImporterInstance(format: ImportType | "bitwardenpasswordprotected", password: string) {
|
private getImporterInstance(
|
||||||
|
format: ImportType | "bitwardenpasswordprotected",
|
||||||
|
promptForPassword_callback: () => Promise<string>
|
||||||
|
) {
|
||||||
if (format == null) {
|
if (format == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -167,12 +175,11 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
case "bitwardencsv":
|
case "bitwardencsv":
|
||||||
return new BitwardenCsvImporter();
|
return new BitwardenCsvImporter();
|
||||||
case "bitwardenjson":
|
case "bitwardenjson":
|
||||||
return new BitwardenJsonImporter(this.cryptoService, this.i18nService);
|
|
||||||
case "bitwardenpasswordprotected":
|
case "bitwardenpasswordprotected":
|
||||||
return new BitwardenPasswordProtectedImporter(
|
return new BitwardenPasswordProtectedImporter(
|
||||||
this.cryptoService,
|
this.cryptoService,
|
||||||
this.i18nService,
|
this.i18nService,
|
||||||
password
|
promptForPassword_callback
|
||||||
);
|
);
|
||||||
case "lastpasscsv":
|
case "lastpasscsv":
|
||||||
case "passboltcsv":
|
case "passboltcsv":
|
||||||
@ -294,8 +301,7 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async postImport(importResult: ImportResult, organizationId: string = null) {
|
private async handleIndividualImport(importResult: ImportResult) {
|
||||||
if (organizationId == null) {
|
|
||||||
const request = new ImportCiphersRequest();
|
const request = new ImportCiphersRequest();
|
||||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||||
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
|
||||||
@ -313,7 +319,9 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return await this.importApiService.postImportCiphers(request);
|
return await this.importApiService.postImportCiphers(request);
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) {
|
||||||
const request = new ImportOrganizationCiphersRequest();
|
const request = new ImportOrganizationCiphersRequest();
|
||||||
for (let i = 0; i < importResult.ciphers.length; i++) {
|
for (let i = 0; i < importResult.ciphers.length; i++) {
|
||||||
importResult.ciphers[i].organizationId = organizationId;
|
importResult.ciphers[i].organizationId = organizationId;
|
||||||
@ -334,7 +342,6 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
}
|
}
|
||||||
return await this.importApiService.postImportOrganizationCiphers(organizationId, request);
|
return await this.importApiService.postImportOrganizationCiphers(organizationId, request);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private badData(c: CipherView) {
|
private badData(c: CipherView) {
|
||||||
return (
|
return (
|
||||||
@ -345,9 +352,9 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): ImportError {
|
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error {
|
||||||
if (errorResponse.validationErrors == null) {
|
if (errorResponse.validationErrors == null) {
|
||||||
return new ImportError(errorResponse.message);
|
return new Error(errorResponse.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
let errorMessage = "";
|
let errorMessage = "";
|
||||||
@ -385,6 +392,6 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
|
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
|
||||||
});
|
});
|
||||||
|
|
||||||
return new ImportError(errorMessage);
|
return new Error(errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user