mirror of
https://github.com/bitwarden/browser
synced 2025-12-11 05:43:41 +00:00
* 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>
486 lines
16 KiB
TypeScript
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]);
|
|
});
|
|
}
|
|
}
|