1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-11 05:43:41 +00:00
Files
browser/libs/importer/src/services/import.service.ts
Daniel James Smith e98cbed437 [AC-1119] [PM-1923] [AC-701] Import into a specified folder or collection (#5683)
* Migrate callouts to the CL ones

* Add folder/collection selection

* Use bitTypography as page header/title

* Migrate submit button to CL

* Migrate fileSelector and fileContents

* Add ability to import into an existing folder/collection

Extended import.service and abstraction to receive importTarget on import()
Pass selectedImportTarget to importService.import()
Wrote unit tests

* Added vault selector, folders/collections selection logic and component library to the import

* Revert changes to the already migrated CL fileSelector, fileContents and header/title

* Fix fileContents input and spacing to submit button

* Use id's instead of name for tghe targetSelector

* Remove unneeded empty line

* Fix import into existing folder/collection

Map ciphers with no folder/no collection to the new rootFolder when selected by the user
Modified and added unit tests

* Added CL to fileSelector and fileInput on vault import

* Added reactive forms and new selector logic to import vault

* Added new texts on Import Vault

* Corrected logic on enable targetSelector

* Removing target selector from being required

* Fixed imports after messing up a merge conflict

* Set No-Folder as default

* Show icons (folder/collection) on targetSelector

* Add icons to vaultSelector

* Set `My Vault` as default of the vaultSelector

* Updates labels based on feedback from design

* Set `My Vault` as default of the vaultSelector pt2

* Improvements to reactive forms on import.component

* Only disabling individual vault import on PersonalOwnership policy

* Use import destination instead of import location

* Add hint to folder/collection dropdown

* Removed required attribute as provided by formGroup

* Display no collection option same as no folder

* Show error on org import with unassigned items

Only admins can have unassigned items (items with no collection)
If these are present in a export/backup file, they should still be imported, to not break existing behaviour. This is limited to admins.
When a member of an org does not set a root collection (no collection option) and any items are unassigned an error message is shown and the import is aborted.

* Removed for-attribute from bit-labels

* Removed bitInput from bit-selects

* Updates to messages.json after PR feedback

* Removed name-attribute from bit-selects

* Removed unneeded variables

* Removed unneeded line break

* Migrate form to use bitSubmit

Rename old submit() to performImport()
Create submit arrow function calling performImport() (which can be overridden/called by org-import.component)
Remove #form and ngNativeValidate
Add bitSubmit and bitFormButton directives
Remove now unneeded loading variable

* Added await to super.performImport()

* Move form check into submit

* AC-1558 - Enable org import with remove individual vault policy

Hide the `My Vault` entry when policy is active
Always check if the policy applies and disable the formGroup if no vault-target is selectable

* [AC-1549] Import page design updates (#5933)

* Display select folder/collection in targetSelector
Filter the no-folder entry from the folderViews-observable
Add labels for the targetSelector placeholders

* Update importTargetHint and remove importTargetOrgHint

* Update language on importUnassignedItemsError

* Add help icon with link to the import documentation

---------

Co-authored-by: Andre Rosado <arosado@bitwarden.com>
2023-08-04 22:05:14 +00:00

486 lines
16 KiB
TypeScript

import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/request/import-organization-ciphers.request";
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
import { CryptoService } from "@bitwarden/common/platform/abstractions/crypto.service";
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
import { Utils } from "@bitwarden/common/platform/misc/utils";
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
import { CollectionService } from "@bitwarden/common/vault/abstractions/collection.service";
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
import { CipherType } from "@bitwarden/common/vault/enums/cipher-type";
import { CipherRequest } from "@bitwarden/common/vault/models/request/cipher.request";
import { CollectionWithIdRequest } from "@bitwarden/common/vault/models/request/collection-with-id.request";
import { FolderWithIdRequest } from "@bitwarden/common/vault/models/request/folder-with-id.request";
import { CipherView } from "@bitwarden/common/vault/models/view/cipher.view";
import { CollectionView } from "@bitwarden/common/vault/models/view/collection.view";
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
import {
AscendoCsvImporter,
AvastCsvImporter,
AvastJsonImporter,
AviraCsvImporter,
BitwardenCsvImporter,
BitwardenPasswordProtectedImporter,
BlackBerryCsvImporter,
BlurCsvImporter,
ButtercupCsvImporter,
ChromeCsvImporter,
ClipperzHtmlImporter,
CodebookCsvImporter,
DashlaneCsvImporter,
DashlaneJsonImporter,
EncryptrCsvImporter,
EnpassCsvImporter,
EnpassJsonImporter,
FirefoxCsvImporter,
FSecureFskImporter,
GnomeJsonImporter,
KasperskyTxtImporter,
KeePass2XmlImporter,
KeePassXCsvImporter,
KeeperCsvImporter,
// KeeperJsonImporter,
LastPassCsvImporter,
LogMeOnceCsvImporter,
MSecureCsvImporter,
MeldiumCsvImporter,
MykiCsvImporter,
NordPassCsvImporter,
OnePassword1PifImporter,
OnePassword1PuxImporter,
OnePasswordMacCsvImporter,
OnePasswordWinCsvImporter,
PadlockCsvImporter,
PassKeepCsvImporter,
PasskyJsonImporter,
PassmanJsonImporter,
PasspackCsvImporter,
PasswordAgentCsvImporter,
PasswordBossJsonImporter,
PasswordDragonXmlImporter,
PasswordSafeXmlImporter,
PasswordWalletTxtImporter,
PsonoJsonImporter,
RememBearCsvImporter,
RoboFormCsvImporter,
SafariCsvImporter,
SafeInCloudXmlImporter,
SaferPassCsvImporter,
SecureSafeCsvImporter,
SplashIdCsvImporter,
StickyPasswordXmlImporter,
TrueKeyCsvImporter,
UpmCsvImporter,
YotiCsvImporter,
ZohoVaultCsvImporter,
} from "../importers";
import { Importer } from "../importers/importer";
import {
featuredImportOptions,
ImportOption,
ImportType,
regularImportOptions,
} from "../models/import-options";
import { ImportResult } from "../models/import-result";
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
export class ImportService implements ImportServiceAbstraction {
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
regularImportOptions = regularImportOptions as readonly ImportOption[];
constructor(
private cipherService: CipherService,
private folderService: FolderService,
private importApiService: ImportApiServiceAbstraction,
private i18nService: I18nService,
private collectionService: CollectionService,
private cryptoService: CryptoService
) {}
getImportOptions(): ImportOption[] {
return this.featuredImportOptions.concat(this.regularImportOptions);
}
async import(
importer: Importer,
fileContents: string,
organizationId: string = null,
selectedImportTarget: string = null,
isUserAdmin: boolean
): Promise<ImportResult> {
let importResult: ImportResult;
try {
importResult = await importer.parse(fileContents);
} catch (error) {
if (error instanceof SyntaxError) {
throw new Error(this.i18nService.t("importFormatError"));
}
throw error;
}
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) {
throw new Error(this.i18nService.t("importNothingError"));
}
if (importResult.ciphers.length > 0) {
const halfway = Math.floor(importResult.ciphers.length / 2);
const last = importResult.ciphers.length - 1;
if (
this.badData(importResult.ciphers[0]) &&
this.badData(importResult.ciphers[halfway]) &&
this.badData(importResult.ciphers[last])
) {
throw new Error(this.i18nService.t("importFormatError"));
}
}
if (organizationId && Utils.isNullOrWhitespace(selectedImportTarget) && !isUserAdmin) {
const hasUnassignedCollections = importResult.ciphers.some(
(c) => !Array.isArray(c.collectionIds) || c.collectionIds.length == 0
);
if (hasUnassignedCollections) {
throw new Error(this.i18nService.t("importUnassignedItemsError"));
}
}
try {
await this.setImportTarget(importResult, organizationId, selectedImportTarget);
if (organizationId != null) {
await this.handleOrganizationalImport(importResult, organizationId);
} else {
await this.handleIndividualImport(importResult);
}
} catch (error) {
const errorResponse = new ErrorResponse(error, 400);
throw this.handleServerError(errorResponse, importResult);
}
return importResult;
}
getImporter(
format: ImportType | "bitwardenpasswordprotected",
promptForPassword_callback: () => Promise<string>,
organizationId: string = null
): Importer {
if (promptForPassword_callback == null) {
return null;
}
const importer = this.getImporterInstance(format, promptForPassword_callback);
if (importer == null) {
return null;
}
importer.organizationId = organizationId;
return importer;
}
private getImporterInstance(
format: ImportType | "bitwardenpasswordprotected",
promptForPassword_callback: () => Promise<string>
) {
if (format == null) {
return null;
}
switch (format) {
case "bitwardencsv":
return new BitwardenCsvImporter();
case "bitwardenjson":
case "bitwardenpasswordprotected":
return new BitwardenPasswordProtectedImporter(
this.cryptoService,
this.i18nService,
promptForPassword_callback
);
case "lastpasscsv":
case "passboltcsv":
return new LastPassCsvImporter();
case "keepassxcsv":
return new KeePassXCsvImporter();
case "aviracsv":
return new AviraCsvImporter();
case "blurcsv":
return new BlurCsvImporter();
case "safeincloudxml":
return new SafeInCloudXmlImporter();
case "padlockcsv":
return new PadlockCsvImporter();
case "keepass2xml":
return new KeePass2XmlImporter();
case "chromecsv":
case "operacsv":
case "vivaldicsv":
return new ChromeCsvImporter();
case "firefoxcsv":
return new FirefoxCsvImporter();
case "upmcsv":
return new UpmCsvImporter();
case "saferpasscsv":
return new SaferPassCsvImporter();
case "safaricsv":
return new SafariCsvImporter();
case "meldiumcsv":
return new MeldiumCsvImporter();
case "1password1pif":
return new OnePassword1PifImporter();
case "1password1pux":
return new OnePassword1PuxImporter();
case "1passwordwincsv":
return new OnePasswordWinCsvImporter();
case "1passwordmaccsv":
return new OnePasswordMacCsvImporter();
case "keepercsv":
return new KeeperCsvImporter();
// case "keeperjson":
// return new KeeperJsonImporter();
case "passworddragonxml":
return new PasswordDragonXmlImporter();
case "enpasscsv":
return new EnpassCsvImporter();
case "enpassjson":
return new EnpassJsonImporter();
case "pwsafexml":
return new PasswordSafeXmlImporter();
case "dashlanecsv":
return new DashlaneCsvImporter();
case "dashlanejson":
return new DashlaneJsonImporter();
case "msecurecsv":
return new MSecureCsvImporter();
case "stickypasswordxml":
return new StickyPasswordXmlImporter();
case "truekeycsv":
return new TrueKeyCsvImporter();
case "clipperzhtml":
return new ClipperzHtmlImporter();
case "roboformcsv":
return new RoboFormCsvImporter();
case "ascendocsv":
return new AscendoCsvImporter();
case "passwordbossjson":
return new PasswordBossJsonImporter();
case "zohovaultcsv":
return new ZohoVaultCsvImporter();
case "splashidcsv":
return new SplashIdCsvImporter();
case "passkeepcsv":
return new PassKeepCsvImporter();
case "gnomejson":
return new GnomeJsonImporter();
case "passwordagentcsv":
return new PasswordAgentCsvImporter();
case "passpackcsv":
return new PasspackCsvImporter();
case "passmanjson":
return new PassmanJsonImporter();
case "avastcsv":
return new AvastCsvImporter();
case "avastjson":
return new AvastJsonImporter();
case "fsecurefsk":
return new FSecureFskImporter();
case "kasperskytxt":
return new KasperskyTxtImporter();
case "remembearcsv":
return new RememBearCsvImporter();
case "passwordwallettxt":
return new PasswordWalletTxtImporter();
case "mykicsv":
return new MykiCsvImporter();
case "securesafecsv":
return new SecureSafeCsvImporter();
case "logmeoncecsv":
return new LogMeOnceCsvImporter();
case "blackberrycsv":
return new BlackBerryCsvImporter();
case "buttercupcsv":
return new ButtercupCsvImporter();
case "codebookcsv":
return new CodebookCsvImporter();
case "encryptrcsv":
return new EncryptrCsvImporter();
case "yoticsv":
return new YotiCsvImporter();
case "nordpasscsv":
return new NordPassCsvImporter();
case "psonojson":
return new PsonoJsonImporter();
case "passkyjson":
return new PasskyJsonImporter();
default:
return null;
}
}
private async handleIndividualImport(importResult: ImportResult) {
const request = new ImportCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.folders != null) {
for (let i = 0; i < importResult.folders.length; i++) {
const f = await this.folderService.encrypt(importResult.folders[i]);
request.folders.push(new FolderWithIdRequest(f));
}
}
if (importResult.folderRelationships != null) {
importResult.folderRelationships.forEach((r) =>
request.folderRelationships.push(new KvpRequest(r[0], r[1]))
);
}
return await this.importApiService.postImportCiphers(request);
}
private async handleOrganizationalImport(importResult: ImportResult, organizationId: string) {
const request = new ImportOrganizationCiphersRequest();
for (let i = 0; i < importResult.ciphers.length; i++) {
importResult.ciphers[i].organizationId = organizationId;
const c = await this.cipherService.encrypt(importResult.ciphers[i]);
request.ciphers.push(new CipherRequest(c));
}
if (importResult.collections != null) {
for (let i = 0; i < importResult.collections.length; i++) {
importResult.collections[i].organizationId = organizationId;
const c = await this.collectionService.encrypt(importResult.collections[i]);
request.collections.push(new CollectionWithIdRequest(c));
}
}
if (importResult.collectionRelationships != null) {
importResult.collectionRelationships.forEach((r) =>
request.collectionRelationships.push(new KvpRequest(r[0], r[1]))
);
}
return await this.importApiService.postImportOrganizationCiphers(organizationId, request);
}
private badData(c: CipherView) {
return (
(c.name == null || c.name === "--") &&
c.type === CipherType.Login &&
c.login != null &&
Utils.isNullOrWhitespace(c.login.password)
);
}
private handleServerError(errorResponse: ErrorResponse, importResult: ImportResult): Error {
if (errorResponse.validationErrors == null) {
return new Error(errorResponse.message);
}
let errorMessage = "";
Object.entries(errorResponse.validationErrors).forEach(([key, value], index) => {
let item;
let itemType;
const i = Number(key.match(/[0-9]+/)[0]);
switch (key.match(/^\w+/)[0]) {
case "Ciphers":
item = importResult.ciphers[i];
itemType = CipherType[item.type];
break;
case "Folders":
item = importResult.folders[i];
itemType = "Folder";
break;
case "Collections":
item = importResult.collections[i];
itemType = "Collection";
break;
default:
return;
}
if (index > 0) {
errorMessage += "\n\n";
}
if (itemType !== "Folder" && itemType !== "Collection") {
errorMessage += "[" + (i + 1) + "] ";
}
errorMessage += "[" + itemType + '] "' + item.name + '": ' + value;
});
return new Error(errorMessage);
}
private async setImportTarget(
importResult: ImportResult,
organizationId: string,
importTarget: string
) {
if (Utils.isNullOrWhitespace(importTarget)) {
return;
}
if (organizationId) {
const collectionViews: CollectionView[] = await this.collectionService.getAllDecrypted();
const targetCollection = collectionViews.find((c) => c.id === importTarget);
const noCollectionRelationShips: [number, number][] = [];
importResult.ciphers.forEach((c, index) => {
if (!Array.isArray(c.collectionIds) || c.collectionIds.length == 0) {
c.collectionIds = [targetCollection.id];
noCollectionRelationShips.push([index, 0]);
}
});
const collections: CollectionView[] = [...importResult.collections];
importResult.collections = [targetCollection];
collections.map((x) => {
const f = new CollectionView();
f.name = `${targetCollection.name}/${x.name}`;
importResult.collections.push(f);
});
const relationships: [number, number][] = [...importResult.collectionRelationships];
importResult.collectionRelationships = [...noCollectionRelationShips];
relationships.map((x) => {
importResult.collectionRelationships.push([x[0], x[1] + 1]);
});
return;
}
const folderViews = await this.folderService.getAllDecryptedFromState();
const targetFolder = folderViews.find((f) => f.id === importTarget);
const noFolderRelationShips: [number, number][] = [];
importResult.ciphers.forEach((c, index) => {
if (Utils.isNullOrEmpty(c.folderId)) {
c.folderId = targetFolder.id;
noFolderRelationShips.push([index, 0]);
}
});
const folders: FolderView[] = [...importResult.folders];
importResult.folders = [targetFolder];
folders.map((x) => {
const newFolderName = `${targetFolder.name}/${x.name}`;
const f = new FolderView();
f.name = newFolderName;
importResult.folders.push(f);
});
const relationships: [number, number][] = [...importResult.folderRelationships];
importResult.folderRelationships = [...noFolderRelationShips];
relationships.map((x) => {
importResult.folderRelationships.push([x[0], x[1] + 1]);
});
}
}