mirror of
https://github.com/bitwarden/browser
synced 2025-12-06 00:13:28 +00:00
[PM-24748][PM-24072] Chromium importer (#16100)
* Add importer dummy lib, add cargo deps for win/mac * Add Chromium importer source from bitwarden/password-access * Mod crypto is no more * Expose some Chromium importer functions via NAPI, replace home with home_dir crate * Add Chromium importer to the main <-> renderer IPC, export all functions from Rust * Add password and notes fields to the imported logins * Fix windows to use homedir instead of home * Return success/failure results * Import from account logins and join * Linux v10 support * Use mod util on Windows * Use mod util on macOS * Refactor to move shared code into chromium.rs * Fix windows * Fix Linux as well * Linux v11 support for Chrome/Gnome, everything is async now * Support multiple browsers on Linux v11 * Move oo7 to Linux * Fix Windows * Fix macOS * Add support for Brave browser in Linux configuration * Add support for Opera browser in Linux configuration * Fix Edge and add Arc on macOS * Add Opera on macOS * Add support for Vivaldi browser in macOS configuration * Add support for Chromium browser in macOS configuration * Fix Edge on Windows * Add Opera on Windows * Add Vivaldi on windows * Add Chromium to supported browsers on Windows * stub out UI options for chromium direct import * call IPC funcs from import-desktop * add notes to chrome csv importer * remove (csv) from import tool names and format item names as hostnames * Add ABE/v20 encryption support * ABE/v20 architecture description * Add a build step to produce admin.exe and service.exe * Add Windows v20/ABE configuration functionality to specify the full path to the admin.exe and service.exe. Use ipc.platform.chromiumImporter.configureWindowsCryptoService to configure the Chromium importer on Windows. * rename ARCHITECTURE.md to README.md * aligns with guidance from architecture re: in-repository documentation. * also fixes a failing lint. * cargo fmt * cargo clippy fix * Declare feature flag for using chromium importer * Linter fix after executing npm run prettier * Use feature flag to guard the use of the chromium importer * Added temporary logging to further debug, why the Angular change detection isn't working as expected * introduce importer metadata; host metadata from service; includes tests * fix cli build * Register autotype module in lib.rs introduce by a bad merge * Fix web build * Fix issue with loaders being undefined and the feature flag turned off * Add missing Chromium support when selecting chromecsv * debugging * remove chromium support from chromecsv metadata * fix default loader selection * [PM-24753] cargo lib file (#16090) * Add new modules * Fix chromium importer * Fix compile bugs for toolchain * remove importer folder * remove IPC code * undo setting change * clippy fixes * cargo fmt * clippy fixes * clippy fixes * clippy fixes * clippy fixes * lint fix * fix release build * Add files in CODEOWNERS * Create tools owned preload.ts * Move chromium-importer.service under tools-ownership * Fix typeError When accessing the Chromium direct import options the file button is hidden, so trying to access it's values will fail * Fix tools owned preload * Remove dead code and redundant truncation * Remove configureWindowsCryptoService function/methods * Clean up cargo files * Fix unused async * Update apps/desktop/desktop_native/bitwarden_chromium_importer/Cargo.toml Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com> * Fix napi deps * fix lints * format * fix linux lint * fix windows lints * format * fix missing `?` * fix a different missing `?` --------- Co-authored-by: Dmitry Yakimenko <detunized@gmail.com> Co-authored-by: Kyle Spearrin <kyle.spearrin@gmail.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: ✨ Audrey ✨ <ajensen@bitwarden.com> Co-authored-by: ✨ Audrey ✨ <audrey@audreyality.com> Co-authored-by: adudek-bw <adudek@bitwarden.com> Co-authored-by: Oscar Hinton <Hinton@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
b957a0c28f
commit
66f5700a75
@@ -37,6 +37,7 @@ export enum FeatureFlag {
|
||||
/* Tools */
|
||||
DesktopSendUIRefresh = "desktop-send-ui-refresh",
|
||||
UseSdkPasswordGenerators = "pm-19976-use-sdk-password-generators",
|
||||
UseChromiumImporter = "pm-23982-chromium-importer",
|
||||
|
||||
/* DIRT */
|
||||
EventBasedOrganizationIntegrations = "event-based-organization-integrations",
|
||||
@@ -79,6 +80,7 @@ export const DefaultFeatureFlagValue = {
|
||||
/* Tools */
|
||||
[FeatureFlag.DesktopSendUIRefresh]: FALSE,
|
||||
[FeatureFlag.UseSdkPasswordGenerators]: FALSE,
|
||||
[FeatureFlag.UseChromiumImporter]: FALSE,
|
||||
|
||||
/* DIRT */
|
||||
[FeatureFlag.EventBasedOrganizationIntegrations]: FALSE,
|
||||
|
||||
178
libs/common/src/tools/providers.spec.ts
Normal file
178
libs/common/src/tools/providers.spec.ts
Normal file
@@ -0,0 +1,178 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
|
||||
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ConfigService } from "../platform/abstractions/config/config.service";
|
||||
import { LogService } from "../platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||
import { StateProvider } from "../platform/state";
|
||||
|
||||
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
|
||||
import { ExtensionRegistry } from "./extension/extension-registry.abstraction";
|
||||
import { ExtensionService } from "./extension/extension.service";
|
||||
import { disabledSemanticLoggerProvider } from "./log";
|
||||
import { createSystemServiceProvider } from "./providers";
|
||||
|
||||
describe("SystemServiceProvider", () => {
|
||||
let mockEncryptor: LegacyEncryptorProvider;
|
||||
let mockState: StateProvider;
|
||||
let mockPolicy: PolicyService;
|
||||
let mockRegistry: ExtensionRegistry;
|
||||
let mockLogger: LogService;
|
||||
let mockEnvironment: MockProxy<PlatformUtilsService>;
|
||||
let mockConfigService: ConfigService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
mockEncryptor = mock<LegacyEncryptorProvider>();
|
||||
mockState = mock<StateProvider>();
|
||||
mockPolicy = mock<PolicyService>();
|
||||
mockRegistry = mock<ExtensionRegistry>();
|
||||
mockLogger = mock<LogService>();
|
||||
mockEnvironment = mock<PlatformUtilsService>();
|
||||
mockConfigService = mock<ConfigService>();
|
||||
});
|
||||
|
||||
describe("createSystemServiceProvider", () => {
|
||||
it("returns object with all required services when called with valid parameters", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result).toHaveProperty("policy", mockPolicy);
|
||||
expect(result).toHaveProperty("extension");
|
||||
expect(result).toHaveProperty("log");
|
||||
expect(result).toHaveProperty("configService", mockConfigService);
|
||||
expect(result).toHaveProperty("environment", mockEnvironment);
|
||||
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||
});
|
||||
|
||||
it("creates ExtensionService with correct dependencies when called", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(true);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||
});
|
||||
|
||||
describe("given development environment", () => {
|
||||
it("uses enableLogForTypes when environment.isDev() returns true", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(true);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
|
||||
expect(result.log).not.toBe(disabledSemanticLoggerProvider);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given production environment", () => {
|
||||
it("uses disabledSemanticLoggerProvider when environment.isDev() returns false", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(mockEnvironment.isDev).toHaveBeenCalledTimes(1);
|
||||
expect(result.log).toBe(disabledSemanticLoggerProvider);
|
||||
});
|
||||
});
|
||||
|
||||
it("configures ExtensionService with encryptor, state, log provider, and now function when called", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
const dateSpy = jest.spyOn(Date, "now");
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result.extension).toBeInstanceOf(ExtensionService);
|
||||
expect(dateSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes through policy service correctly when called", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result.policy).toBe(mockPolicy);
|
||||
});
|
||||
|
||||
it("passes through configService correctly when called", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result.configService).toBe(mockConfigService);
|
||||
});
|
||||
|
||||
it("passes through environment service correctly when called", () => {
|
||||
mockEnvironment.isDev.mockReturnValue(false);
|
||||
|
||||
const result = createSystemServiceProvider(
|
||||
mockEncryptor,
|
||||
mockState,
|
||||
mockPolicy,
|
||||
mockRegistry,
|
||||
mockLogger,
|
||||
mockEnvironment,
|
||||
mockConfigService,
|
||||
);
|
||||
|
||||
expect(result.environment).toBe(mockEnvironment);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,15 @@
|
||||
import { LogService } from "@bitwarden/logging";
|
||||
import { BitwardenClient } from "@bitwarden/sdk-internal";
|
||||
import { StateProvider } from "@bitwarden/state";
|
||||
|
||||
import { PolicyService } from "../admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { ConfigService } from "../platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "../platform/abstractions/platform-utils.service";
|
||||
|
||||
import { LegacyEncryptorProvider } from "./cryptography/legacy-encryptor-provider";
|
||||
import { ExtensionRegistry } from "./extension/extension-registry.abstraction";
|
||||
import { ExtensionService } from "./extension/extension.service";
|
||||
import { LogProvider } from "./log";
|
||||
import { disabledSemanticLoggerProvider, enableLogForTypes, LogProvider } from "./log";
|
||||
|
||||
/** Provides access to commonly-used cross-cutting services. */
|
||||
export type SystemServiceProvider = {
|
||||
@@ -20,6 +25,42 @@ export type SystemServiceProvider = {
|
||||
/** Config Service to determine flag features */
|
||||
readonly configService: ConfigService;
|
||||
|
||||
/** Platform Service to inspect runtime environment */
|
||||
readonly environment: PlatformUtilsService;
|
||||
|
||||
/** SDK Service */
|
||||
readonly sdk: BitwardenClient;
|
||||
readonly sdk?: BitwardenClient;
|
||||
};
|
||||
|
||||
/** Constructs a system service provider. */
|
||||
export function createSystemServiceProvider(
|
||||
encryptor: LegacyEncryptorProvider,
|
||||
state: StateProvider,
|
||||
policy: PolicyService,
|
||||
registry: ExtensionRegistry,
|
||||
logger: LogService,
|
||||
environment: PlatformUtilsService,
|
||||
configService: ConfigService,
|
||||
): SystemServiceProvider {
|
||||
let log: LogProvider;
|
||||
if (environment.isDev()) {
|
||||
log = enableLogForTypes(logger, []);
|
||||
} else {
|
||||
log = disabledSemanticLoggerProvider;
|
||||
}
|
||||
|
||||
const extension = new ExtensionService(registry, {
|
||||
encryptor,
|
||||
state,
|
||||
log,
|
||||
now: Date.now,
|
||||
});
|
||||
|
||||
return {
|
||||
policy,
|
||||
extension,
|
||||
log,
|
||||
configService,
|
||||
environment,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
<div [formGroup]="formGroup">
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "browserProfile" | i18n }}</bit-label>
|
||||
<bit-select formControlName="profile">
|
||||
<bit-option *ngFor="let p of profileList" [value]="p.id" [label]="p.name" />
|
||||
</bit-select>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
167
libs/importer/src/components/chrome/import-chrome.component.ts
Normal file
167
libs/importer/src/components/chrome/import-chrome.component.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { CommonModule } from "@angular/common";
|
||||
import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from "@angular/core";
|
||||
import {
|
||||
AsyncValidatorFn,
|
||||
ControlContainer,
|
||||
FormBuilder,
|
||||
FormGroup,
|
||||
ReactiveFormsModule,
|
||||
Validators,
|
||||
} from "@angular/forms";
|
||||
import * as papa from "papaparse";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import {
|
||||
CalloutModule,
|
||||
CheckboxModule,
|
||||
FormFieldModule,
|
||||
IconButtonModule,
|
||||
SelectModule,
|
||||
TypographyModule,
|
||||
} from "@bitwarden/components";
|
||||
|
||||
import { ImportType } from "../../models";
|
||||
|
||||
@Component({
|
||||
selector: "import-chrome",
|
||||
templateUrl: "import-chrome.component.html",
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
JslibModule,
|
||||
CalloutModule,
|
||||
TypographyModule,
|
||||
FormFieldModule,
|
||||
ReactiveFormsModule,
|
||||
IconButtonModule,
|
||||
CheckboxModule,
|
||||
SelectModule,
|
||||
],
|
||||
})
|
||||
export class ImportChromeComponent implements OnInit, OnDestroy {
|
||||
private _parentFormGroup: FormGroup;
|
||||
protected formGroup = this.formBuilder.group({
|
||||
profile: [
|
||||
"",
|
||||
{
|
||||
nonNullable: true,
|
||||
validators: [Validators.required],
|
||||
asyncValidators: [this.validateAndEmitData()],
|
||||
updateOn: "submit",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
profileList: { id: string; name: string }[] = [];
|
||||
|
||||
@Input()
|
||||
format: ImportType;
|
||||
|
||||
@Input()
|
||||
onLoadProfilesFromBrowser: (browser: string) => Promise<any[]>;
|
||||
|
||||
@Input()
|
||||
onImportFromBrowser: (browser: string, profile: string) => Promise<any[]>;
|
||||
|
||||
@Output() csvDataLoaded = new EventEmitter<string>();
|
||||
|
||||
constructor(
|
||||
private formBuilder: FormBuilder,
|
||||
private controlContainer: ControlContainer,
|
||||
private logService: LogService,
|
||||
private i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async ngOnInit(): Promise<void> {
|
||||
this._parentFormGroup = this.controlContainer.control as FormGroup;
|
||||
this._parentFormGroup.addControl("chromeOptions", this.formGroup);
|
||||
this.profileList = await this.onLoadProfilesFromBrowser(this.getBrowserName());
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this._parentFormGroup.removeControl("chromeOptions");
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to login to the provided Chrome email and retrieve account contents.
|
||||
* Will return a validation error if unable to login or fetch.
|
||||
* Emits account contents to `csvDataLoaded`
|
||||
*/
|
||||
validateAndEmitData(): AsyncValidatorFn {
|
||||
return async () => {
|
||||
try {
|
||||
const logins = await this.onImportFromBrowser(
|
||||
this.getBrowserName(),
|
||||
this.formGroup.controls.profile.value,
|
||||
);
|
||||
if (logins.length === 0) {
|
||||
throw "nothing to import";
|
||||
}
|
||||
const chromeLogins: ChromeLogin[] = [];
|
||||
for (const l of logins) {
|
||||
if (l.login != null) {
|
||||
chromeLogins.push(new ChromeLogin(l.login));
|
||||
}
|
||||
}
|
||||
const csvData = papa.unparse(chromeLogins);
|
||||
this.csvDataLoaded.emit(csvData);
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logService.error(`Chromium importer error: ${error}`);
|
||||
return {
|
||||
errors: {
|
||||
message: this.i18nService.t(this.getValidationErrorI18nKey(error)),
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private getValidationErrorI18nKey(error: any): string {
|
||||
const message = typeof error === "string" ? error : error?.message;
|
||||
switch (message) {
|
||||
default:
|
||||
return "errorOccurred";
|
||||
}
|
||||
}
|
||||
|
||||
private getBrowserName(): string {
|
||||
if (this.format === "edgecsv") {
|
||||
return "Microsoft Edge";
|
||||
} else if (this.format === "operacsv") {
|
||||
return "Opera";
|
||||
} else if (this.format === "bravecsv") {
|
||||
return "Brave";
|
||||
} else if (this.format === "vivaldicsv") {
|
||||
return "Vivaldi";
|
||||
}
|
||||
return "Chrome";
|
||||
}
|
||||
}
|
||||
|
||||
class ChromeLogin {
|
||||
name: string;
|
||||
url: string;
|
||||
username: string;
|
||||
password: string;
|
||||
note: string;
|
||||
|
||||
constructor(login: any) {
|
||||
const url = Utils.getUrl(login?.url);
|
||||
if (url != null) {
|
||||
this.name = new URL(url).hostname;
|
||||
}
|
||||
if (this.name == null) {
|
||||
this.name = login.url;
|
||||
}
|
||||
this.url = login.url;
|
||||
this.username = login.username;
|
||||
this.password = login.password;
|
||||
this.note = login.note;
|
||||
}
|
||||
}
|
||||
1
libs/importer/src/components/chrome/index.ts
Normal file
1
libs/importer/src/components/chrome/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export { ImportChromeComponent } from "./import-chrome.component";
|
||||
@@ -169,27 +169,41 @@
|
||||
"Export" to save the JSON file.
|
||||
</ng-container>
|
||||
-->
|
||||
<ng-container
|
||||
*ngIf="
|
||||
format === 'chromecsv' ||
|
||||
format === 'operacsv' ||
|
||||
format === 'vivaldicsv' ||
|
||||
format === 'edgecsv'
|
||||
"
|
||||
>
|
||||
<ng-container *ngIf="showChromiumInstructions$ | async">
|
||||
<span *ngIf="format !== 'chromecsv'">
|
||||
The process is exactly the same as importing from Google Chrome.
|
||||
</span>
|
||||
See detailed instructions on our help site at
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://bitwarden.com/help/import-from-chrome/"
|
||||
>
|
||||
https://bitwarden.com/help/import-from-chrome/</a
|
||||
<p>
|
||||
See detailed instructions on our help site at
|
||||
<a
|
||||
bitLink
|
||||
linkType="primary"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://bitwarden.com/help/import-from-chrome/"
|
||||
>
|
||||
https://bitwarden.com/help/import-from-chrome/</a
|
||||
>
|
||||
</p>
|
||||
<bit-radio-group
|
||||
[hidden]="!(browserImporterAvailable$ | async)"
|
||||
formControlName="chromiumLoader"
|
||||
>
|
||||
<bit-radio-button
|
||||
class="tw-block"
|
||||
id="import_bit-radio-button_chrome-browser"
|
||||
value="chromium"
|
||||
>
|
||||
<bit-label>{{ "importDirectlyFromBrowser" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
<bit-radio-button
|
||||
class="tw-block"
|
||||
id="import_bit-radio-button_chrome-file"
|
||||
value="file"
|
||||
>
|
||||
<bit-label>{{ "importFromCSV" | i18n }}</bit-label>
|
||||
</bit-radio-button>
|
||||
</bit-radio-group>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="format === 'firefoxcsv'">
|
||||
See detailed instructions on our help site at
|
||||
@@ -440,12 +454,20 @@
|
||||
previously chosen.
|
||||
</ng-container>
|
||||
</bit-callout>
|
||||
<import-lastpass
|
||||
*ngIf="showLastPassOptions"
|
||||
[formGroup]="formGroup"
|
||||
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
|
||||
></import-lastpass>
|
||||
<div [hidden]="showLastPassOptions">
|
||||
@if (showLastPassOptions) {
|
||||
<import-lastpass
|
||||
[formGroup]="formGroup"
|
||||
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
|
||||
></import-lastpass>
|
||||
} @else if (showChromiumOptions$ | async) {
|
||||
<import-chrome
|
||||
[formGroup]="formGroup"
|
||||
[onImportFromBrowser]="this.onImportFromBrowser"
|
||||
[onLoadProfilesFromBrowser]="this.onLoadProfilesFromBrowser"
|
||||
[format]="this.format"
|
||||
(csvDataLoaded)="this.formGroup.controls.fileContents.setValue($event)"
|
||||
></import-chrome>
|
||||
} @else {
|
||||
<bit-form-field>
|
||||
<bit-label>{{ "selectImportFile" | i18n }}</bit-label>
|
||||
<div class="file-selector tw-pt-2 tw-pb-1 tw-break-words">
|
||||
@@ -473,7 +495,7 @@
|
||||
formControlName="fileContents"
|
||||
></textarea>
|
||||
</bit-form-field>
|
||||
</div>
|
||||
}
|
||||
</bit-card>
|
||||
</bit-section>
|
||||
</form>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CommonModule } from "@angular/common";
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
DestroyRef,
|
||||
EventEmitter,
|
||||
Inject,
|
||||
Input,
|
||||
@@ -13,17 +14,23 @@ import {
|
||||
Output,
|
||||
ViewChild,
|
||||
} from "@angular/core";
|
||||
import { takeUntilDestroyed } from "@angular/core/rxjs-interop";
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from "@angular/forms";
|
||||
import * as JSZip from "jszip";
|
||||
import { Observable, Subject, lastValueFrom, combineLatest, firstValueFrom } from "rxjs";
|
||||
import {
|
||||
Observable,
|
||||
Subject,
|
||||
lastValueFrom,
|
||||
combineLatest,
|
||||
firstValueFrom,
|
||||
BehaviorSubject,
|
||||
} from "rxjs";
|
||||
import { combineLatestWith, filter, map, switchMap, takeUntil } from "rxjs/operators";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import {
|
||||
getOrganizationById,
|
||||
OrganizationService,
|
||||
@@ -34,14 +41,10 @@ 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 { ClientType } from "@bitwarden/common/enums";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { SyncService } from "@bitwarden/common/vault/abstractions/sync/sync.service.abstraction";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
@@ -62,49 +65,20 @@ import {
|
||||
ToastService,
|
||||
LinkModule,
|
||||
} from "@bitwarden/components";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
||||
import { ImportOption, ImportResult, ImportType } from "../models";
|
||||
import {
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportCollectionServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "../services";
|
||||
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
|
||||
|
||||
import { ImportChromeComponent } from "./chrome";
|
||||
import {
|
||||
FilePasswordPromptComponent,
|
||||
ImportErrorDialogComponent,
|
||||
ImportSuccessDialogComponent,
|
||||
} from "./dialog";
|
||||
import { ImporterProviders } from "./importer-providers";
|
||||
import { ImportLastPassComponent } from "./lastpass";
|
||||
|
||||
const safeProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: ImportApiServiceAbstraction,
|
||||
useClass: ImportApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ImportServiceAbstraction,
|
||||
useClass: ImportService,
|
||||
deps: [
|
||||
CipherService,
|
||||
FolderService,
|
||||
ImportApiServiceAbstraction,
|
||||
I18nService,
|
||||
CollectionService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
PinServiceAbstraction,
|
||||
AccountService,
|
||||
SdkService,
|
||||
RestrictedItemTypesService,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@Component({
|
||||
selector: "tools-import",
|
||||
templateUrl: "import.component.html",
|
||||
@@ -118,6 +92,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SelectModule,
|
||||
CalloutModule,
|
||||
ReactiveFormsModule,
|
||||
ImportChromeComponent,
|
||||
ImportLastPassComponent,
|
||||
RadioButtonModule,
|
||||
CardComponent,
|
||||
@@ -125,7 +100,7 @@ const safeProviders: SafeProvider[] = [
|
||||
SectionComponent,
|
||||
LinkModule,
|
||||
],
|
||||
providers: safeProviders,
|
||||
providers: ImporterProviders,
|
||||
})
|
||||
export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
featuredImportOptions: ImportOption[];
|
||||
@@ -160,6 +135,12 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
@Input()
|
||||
onLoadProfilesFromBrowser: (browser: string) => Promise<any[]>;
|
||||
|
||||
@Input()
|
||||
onImportFromBrowser: (browser: string, profile: string) => Promise<any[]>;
|
||||
|
||||
protected organization: Organization;
|
||||
protected destroy$ = new Subject<void>();
|
||||
|
||||
@@ -184,6 +165,8 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
fileContents: [],
|
||||
file: [],
|
||||
lastPassType: ["direct" as "csv" | "direct"],
|
||||
// FIXME: once the flag is disabled this should initialize to `Strategy.browser`
|
||||
chromiumLoader: [Loader.file as DataLoader],
|
||||
});
|
||||
|
||||
@ViewChild(BitSubmitDirective)
|
||||
@@ -208,6 +191,26 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
});
|
||||
}
|
||||
|
||||
private importer$ = new BehaviorSubject<ImporterMetadata | undefined>(undefined);
|
||||
|
||||
/** emits `true` when the chromium instruction block should be visible. */
|
||||
protected readonly showChromiumInstructions$ = this.importer$.pipe(
|
||||
map((importer) => importer?.instructions === Instructions.chromium),
|
||||
);
|
||||
|
||||
/** emits `true` when direct browser import is available. */
|
||||
// FIXME: use the capabilities list to populate `chromiumLoader` and replace the explicit
|
||||
// strategy check with a check for multiple loaders
|
||||
protected readonly browserImporterAvailable$ = this.importer$.pipe(
|
||||
map((importer) => (importer?.loaders ?? []).includes(Loader.chromium)),
|
||||
);
|
||||
|
||||
/** emits `true` when the chromium loader is selected. */
|
||||
protected readonly showChromiumOptions$ =
|
||||
this.formGroup.controls.chromiumLoader.valueChanges.pipe(
|
||||
map((chromiumLoader) => chromiumLoader === Loader.chromium),
|
||||
);
|
||||
|
||||
constructor(
|
||||
protected i18nService: I18nService,
|
||||
protected importService: ImportServiceAbstraction,
|
||||
@@ -226,6 +229,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected toastService: ToastService,
|
||||
protected accountService: AccountService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private destroyRef: DestroyRef,
|
||||
) {}
|
||||
|
||||
protected get importBlockedByPolicy(): boolean {
|
||||
@@ -246,6 +250,23 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
async ngOnInit() {
|
||||
this.setImportOptions();
|
||||
|
||||
this.importService
|
||||
.metadata$(this.formGroup.controls.format.valueChanges)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (importer) => {
|
||||
this.importer$.next(importer);
|
||||
|
||||
// when an importer is defined, the loader needs to be set to a value from
|
||||
// its list.
|
||||
const loader = importer.loaders.includes(Loader.chromium)
|
||||
? Loader.chromium
|
||||
: importer.loaders?.[0];
|
||||
this.formGroup.controls.chromiumLoader.setValue(loader ?? Loader.file);
|
||||
},
|
||||
error: (err: unknown) => this.logService.error("an error occurred", err),
|
||||
});
|
||||
|
||||
if (this.organizationId) {
|
||||
await this.handleOrganizationImportInit();
|
||||
} else {
|
||||
@@ -578,7 +599,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
|
||||
private async setImportContents(): Promise<string> {
|
||||
const fileEl = document.getElementById("import_input_file") as HTMLInputElement;
|
||||
const files = fileEl.files;
|
||||
const files = fileEl?.files;
|
||||
let fileContents = this.formGroup.controls.fileContents.value;
|
||||
|
||||
if (files != null && files.length > 0) {
|
||||
|
||||
91
libs/importer/src/components/importer-providers.ts
Normal file
91
libs/importer/src/components/importer-providers.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService } from "@bitwarden/admin-console/common";
|
||||
import { safeProvider, SafeProvider } from "@bitwarden/angular/platform/utils/safe-provider";
|
||||
import { ApiService } from "@bitwarden/common/abstractions/api.service";
|
||||
import { PolicyService } from "@bitwarden/common/admin-console/abstractions/policy/policy.service.abstraction";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { KeyServiceLegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/key-service-legacy-encryptor-provider";
|
||||
import { LegacyEncryptorProvider } from "@bitwarden/common/tools/cryptography/legacy-encryptor-provider";
|
||||
import { ExtensionRegistry } from "@bitwarden/common/tools/extension/extension-registry.abstraction";
|
||||
import { buildExtensionRegistry } from "@bitwarden/common/tools/extension/factory";
|
||||
import {
|
||||
createSystemServiceProvider,
|
||||
SystemServiceProvider,
|
||||
} from "@bitwarden/common/tools/providers";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
import { RestrictedItemTypesService } from "@bitwarden/common/vault/services/restricted-item-types.service";
|
||||
import { KeyService } from "@bitwarden/key-management";
|
||||
import { StateProvider } from "@bitwarden/state";
|
||||
import { SafeInjectionToken } from "@bitwarden/ui-common";
|
||||
|
||||
import {
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "../services";
|
||||
|
||||
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
|
||||
// to a general module.
|
||||
const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>("SystemServices");
|
||||
|
||||
/** Import service factories */
|
||||
export const ImporterProviders: SafeProvider[] = [
|
||||
safeProvider({
|
||||
provide: ImportApiServiceAbstraction,
|
||||
useClass: ImportApiService,
|
||||
deps: [ApiService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: LegacyEncryptorProvider,
|
||||
useClass: KeyServiceLegacyEncryptorProvider,
|
||||
deps: [EncryptService, KeyService],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ExtensionRegistry,
|
||||
useFactory: () => {
|
||||
return buildExtensionRegistry();
|
||||
},
|
||||
deps: [],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: SYSTEM_SERVICE_PROVIDER,
|
||||
useFactory: createSystemServiceProvider,
|
||||
deps: [
|
||||
LegacyEncryptorProvider,
|
||||
StateProvider,
|
||||
PolicyService,
|
||||
ExtensionRegistry,
|
||||
LogService,
|
||||
PlatformUtilsService,
|
||||
ConfigService,
|
||||
],
|
||||
}),
|
||||
safeProvider({
|
||||
provide: ImportServiceAbstraction,
|
||||
useClass: ImportService,
|
||||
deps: [
|
||||
CipherService,
|
||||
FolderService,
|
||||
ImportApiServiceAbstraction,
|
||||
I18nService,
|
||||
CollectionService,
|
||||
KeyService,
|
||||
EncryptService,
|
||||
PinServiceAbstraction,
|
||||
AccountService,
|
||||
RestrictedItemTypesService,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
],
|
||||
}),
|
||||
];
|
||||
@@ -24,6 +24,7 @@ export class ChromeCsvImporter extends BaseImporter implements Importer {
|
||||
cipher.login.username = this.getValueOrDefault(value.username);
|
||||
cipher.login.password = this.getValueOrDefault(value.password);
|
||||
cipher.login.uris = this.makeUriArray(value.url);
|
||||
cipher.notes = this.getValueOrDefault(value.note);
|
||||
this.cleanupCipher(cipher);
|
||||
result.ciphers.push(cipher);
|
||||
});
|
||||
|
||||
15
libs/importer/src/metadata/availability.ts
Normal file
15
libs/importer/src/metadata/availability.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { Loader } from "./data";
|
||||
import { DataLoader } from "./types";
|
||||
|
||||
/** Describes which loaders are supported on each client */
|
||||
export const LoaderAvailability: Record<DataLoader, ClientType[]> = deepFreeze({
|
||||
[Loader.chromium]: [ClientType.Desktop],
|
||||
[Loader.download]: [ClientType.Browser],
|
||||
[Loader.file]: [ClientType.Browser, ClientType.Desktop, ClientType.Web, ClientType.Cli],
|
||||
|
||||
// FIXME: enable IPC importer on `ClientType.Desktop` once it's ready
|
||||
[Loader.ipc]: [],
|
||||
});
|
||||
27
libs/importer/src/metadata/data.ts
Normal file
27
libs/importer/src/metadata/data.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/** Mechanisms that load data into the importer. */
|
||||
export const Loader = Object.freeze({
|
||||
/** Data loaded from a file provided by the user/ */
|
||||
file: "file",
|
||||
|
||||
/** Data loaded directly from the chromium browser's data store */
|
||||
chromium: "chromium",
|
||||
|
||||
/** Data provided through an importer ipc channel (e.g. Bitwarden bridge) */
|
||||
ipc: "ipc",
|
||||
|
||||
/** Data provided through direct file download (e.g. a LastPass export) */
|
||||
download: "download",
|
||||
});
|
||||
|
||||
/** Re-branded products often leave their exporters unaltered; when that occurs,
|
||||
* `Instructions` lets us group them together.
|
||||
*
|
||||
* @remarks Instructions values must be mutually exclusive from Loader's values.
|
||||
*/
|
||||
export const Instructions = Object.freeze({
|
||||
/** the instructions are unique to the import type */
|
||||
unique: "unique",
|
||||
|
||||
/** shared chromium instructions */
|
||||
chromium: "chromium",
|
||||
});
|
||||
27
libs/importer/src/metadata/importers.ts
Normal file
27
libs/importer/src/metadata/importers.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { Loader, Instructions } from "./data";
|
||||
import { ImporterMetadata } from "./types";
|
||||
|
||||
// FIXME: load this data from rust code
|
||||
const importers = [
|
||||
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
|
||||
{ id: "chromecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{ id: "operacsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{
|
||||
id: "vivaldicsv",
|
||||
loaders: [Loader.file, Loader.chromium],
|
||||
instructions: Instructions.chromium,
|
||||
},
|
||||
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
|
||||
// FIXME: add other formats and remove `Partial` from export
|
||||
] as const;
|
||||
|
||||
/** Describes which loaders are available for each import type */
|
||||
export const Importers: Partial<Record<ImportType, ImporterMetadata>> = deepFreeze(
|
||||
Object.fromEntries(importers.map((i) => [i.id, i])),
|
||||
);
|
||||
4
libs/importer/src/metadata/index.ts
Normal file
4
libs/importer/src/metadata/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./availability";
|
||||
export * from "./data";
|
||||
export * from "./types";
|
||||
export * from "./importers";
|
||||
20
libs/importer/src/metadata/types.ts
Normal file
20
libs/importer/src/metadata/types.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { Instructions, Loader } from "./data";
|
||||
|
||||
/** Mechanisms that load data into the importer. */
|
||||
export type DataLoader = (typeof Loader)[keyof typeof Loader];
|
||||
|
||||
export type InstructionLink = (typeof Instructions)[keyof typeof Instructions];
|
||||
|
||||
/** Mechanisms that load data into the importer. */
|
||||
export type ImporterMetadata = {
|
||||
/** Identifies the importer */
|
||||
type: ImportType;
|
||||
|
||||
/** Identifies the instructions for the importer; this defaults to `unique`. */
|
||||
instructions?: InstructionLink;
|
||||
|
||||
/** Describes the strategies used to obtain imported data */
|
||||
loaders: DataLoader[];
|
||||
};
|
||||
@@ -6,7 +6,7 @@ export interface ImportOption {
|
||||
export const featuredImportOptions = [
|
||||
{ id: "bitwardenjson", name: "Bitwarden (json)" },
|
||||
{ id: "bitwardencsv", name: "Bitwarden (csv)" },
|
||||
{ id: "chromecsv", name: "Chrome (csv)" },
|
||||
{ id: "chromecsv", name: "Chrome" },
|
||||
{ id: "dashlanecsv", name: "Dashlane (csv)" },
|
||||
{ id: "firefoxcsv", name: "Firefox (csv)" },
|
||||
{ id: "keepass2xml", name: "KeePass 2 (xml)" },
|
||||
@@ -46,9 +46,10 @@ export const regularImportOptions = [
|
||||
{ id: "ascendocsv", name: "Ascendo DataVault (csv)" },
|
||||
{ id: "meldiumcsv", name: "Meldium (csv)" },
|
||||
{ id: "passkeepcsv", name: "PassKeep (csv)" },
|
||||
{ id: "edgecsv", name: "Edge (csv)" },
|
||||
{ id: "operacsv", name: "Opera (csv)" },
|
||||
{ id: "vivaldicsv", name: "Vivaldi (csv)" },
|
||||
{ id: "edgecsv", name: "Edge" },
|
||||
{ id: "operacsv", name: "Opera" },
|
||||
{ id: "vivaldicsv", name: "Vivaldi" },
|
||||
{ id: "bravecsv", name: "Brave" },
|
||||
{ id: "gnomejson", name: "GNOME Passwords and Keys/Seahorse (json)" },
|
||||
{ id: "blurcsv", name: "Blur (csv)" },
|
||||
{ id: "passwordagentcsv", name: "Password Agent (csv)" },
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata } from "../metadata";
|
||||
import { ImportOption, ImportType } from "../models/import-options";
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
@@ -13,6 +16,10 @@ export abstract class ImportServiceAbstraction {
|
||||
featuredImportOptions: readonly ImportOption[];
|
||||
regularImportOptions: readonly ImportOption[];
|
||||
getImportOptions: () => ImportOption[];
|
||||
|
||||
/** describes the features supported by a format */
|
||||
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||
|
||||
import: (
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { BehaviorSubject, Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { MockSdkService } from "@bitwarden/common/platform/spec/mock-sdk.service";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -19,6 +25,8 @@ import { KeyService } from "@bitwarden/key-management";
|
||||
|
||||
import { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata, Instructions, Loader } from "../metadata";
|
||||
import { ImportType } from "../models";
|
||||
import { ImportResult } from "../models/import-result";
|
||||
|
||||
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
||||
@@ -35,8 +43,8 @@ describe("ImportService", () => {
|
||||
let encryptService: MockProxy<EncryptService>;
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let sdkService: MockSdkService;
|
||||
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherService = mock<CipherService>();
|
||||
@@ -47,9 +55,20 @@ describe("ImportService", () => {
|
||||
keyService = mock<KeyService>();
|
||||
encryptService = mock<EncryptService>();
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
sdkService = new MockSdkService();
|
||||
restrictedItemTypesService = mock<RestrictedItemTypesService>();
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockReturnValue(new BehaviorSubject(false));
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
|
||||
});
|
||||
|
||||
importService = new ImportService(
|
||||
cipherService,
|
||||
folderService,
|
||||
@@ -60,8 +79,8 @@ describe("ImportService", () => {
|
||||
encryptService,
|
||||
pinService,
|
||||
accountService,
|
||||
sdkService,
|
||||
restrictedItemTypesService,
|
||||
systemServiceProvider,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -249,6 +268,170 @@ describe("ImportService", () => {
|
||||
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("metadata$", () => {
|
||||
let featureFlagSubject: BehaviorSubject<boolean>;
|
||||
let typeSubject: Subject<ImportType>;
|
||||
let mockLogger: { debug: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
featureFlagSubject = new BehaviorSubject(false);
|
||||
typeSubject = new Subject<ImportType>();
|
||||
mockLogger = { debug: jest.fn() };
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
configService.getFeatureFlag$.mockReturnValue(featureFlagSubject);
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue(mockLogger),
|
||||
});
|
||||
|
||||
// Recreate the service with the updated mocks for logging tests
|
||||
importService = new ImportService(
|
||||
cipherService,
|
||||
folderService,
|
||||
importApiService,
|
||||
i18nService,
|
||||
collectionService,
|
||||
keyService,
|
||||
encryptService,
|
||||
pinService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
systemServiceProvider,
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
featureFlagSubject.complete();
|
||||
typeSubject.complete();
|
||||
});
|
||||
|
||||
it("should emit metadata when type$ emits", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result).toEqual({
|
||||
type: testType,
|
||||
loaders: expect.any(Array),
|
||||
instructions: Instructions.chromium,
|
||||
});
|
||||
expect(result.type).toBe(testType);
|
||||
});
|
||||
|
||||
it("should include all loaders when chromium feature flag is enabled", async () => {
|
||||
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
|
||||
featureFlagSubject.next(true);
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result.loaders).toContain(Loader.chromium);
|
||||
expect(result.loaders).toContain(Loader.file);
|
||||
});
|
||||
|
||||
it("should exclude chromium loader when feature flag is disabled", async () => {
|
||||
const testType: ImportType = "bravecsv"; // bravecsv supports both file and chromium loaders
|
||||
featureFlagSubject.next(false);
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
const result = await metadataPromise;
|
||||
|
||||
expect(result.loaders).not.toContain(Loader.chromium);
|
||||
expect(result.loaders).toContain(Loader.file);
|
||||
});
|
||||
|
||||
it("should update when type$ changes", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
typeSubject.next("chromecsv");
|
||||
typeSubject.next("bravecsv");
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions).toHaveLength(2);
|
||||
expect(emissions[0].type).toBe("chromecsv");
|
||||
expect(emissions[1].type).toBe("bravecsv");
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should update when feature flag changes", async () => {
|
||||
const testType: ImportType = "bravecsv"; // Use bravecsv which supports chromium loader
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
|
||||
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
typeSubject.next(testType);
|
||||
featureFlagSubject.next(true);
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions).toHaveLength(2);
|
||||
expect(emissions[0].loaders).not.toContain(Loader.chromium);
|
||||
expect(emissions[1].loaders).toContain(Loader.chromium);
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should update when both type$ and feature flag change", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
|
||||
const subscription = importService.metadata$(typeSubject).subscribe((metadata) => {
|
||||
emissions.push(metadata);
|
||||
});
|
||||
|
||||
// Initial emission
|
||||
typeSubject.next("chromecsv");
|
||||
|
||||
// Change both at the same time
|
||||
featureFlagSubject.next(true);
|
||||
typeSubject.next("bravecsv");
|
||||
|
||||
// Wait for emissions
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
|
||||
expect(emissions.length).toBeGreaterThanOrEqual(2);
|
||||
const lastEmission = emissions[emissions.length - 1];
|
||||
expect(lastEmission.type).toBe("bravecsv");
|
||||
|
||||
subscription.unsubscribe();
|
||||
});
|
||||
|
||||
it("should log debug information with correct data", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(importService.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
await metadataPromise;
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
{ importType: testType, capabilities: expect.any(Object) },
|
||||
"capabilities updated",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function createCipher(options: Partial<CipherView> = {}) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// FIXME: Update this file to be type safe and remove this and next line
|
||||
// @ts-strict-ignore
|
||||
import { firstValueFrom, map } from "rxjs";
|
||||
import { combineLatest, firstValueFrom, map, Observable } from "rxjs";
|
||||
|
||||
// This import has been flagged as unallowed for this class. It may be involved in a circular dependency loop.
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CollectionView,
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
|
||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
||||
@@ -17,8 +18,9 @@ import { ImportOrganizationCiphersRequest } from "@bitwarden/common/models/reque
|
||||
import { KvpRequest } from "@bitwarden/common/models/request/kvp.request";
|
||||
import { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||
import { SdkService } from "@bitwarden/common/platform/abstractions/sdk/sdk.service";
|
||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { OrganizationId } from "@bitwarden/common/types/guid";
|
||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||
@@ -95,6 +97,7 @@ import {
|
||||
PasswordDepot17XmlImporter,
|
||||
} from "../importers";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata, Importers, Loader } from "../metadata";
|
||||
import {
|
||||
featuredImportOptions,
|
||||
ImportOption,
|
||||
@@ -104,12 +107,15 @@ import {
|
||||
import { ImportResult } from "../models/import-result";
|
||||
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
||||
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
||||
import { availableLoaders as availableLoaders } from "../util";
|
||||
|
||||
export class ImportService implements ImportServiceAbstraction {
|
||||
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
||||
|
||||
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
||||
|
||||
private logger: SemanticLogger;
|
||||
|
||||
constructor(
|
||||
private cipherService: CipherService,
|
||||
private folderService: FolderService,
|
||||
@@ -120,14 +126,42 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
private encryptService: EncryptService,
|
||||
private pinService: PinServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
private sdkService: SdkService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
) {}
|
||||
private system: SystemServiceProvider,
|
||||
) {
|
||||
this.logger = system.log({ type: "ImportService" });
|
||||
}
|
||||
|
||||
getImportOptions(): ImportOption[] {
|
||||
return this.featuredImportOptions.concat(this.regularImportOptions);
|
||||
}
|
||||
|
||||
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
|
||||
const browserEnabled$ = this.system.configService.getFeatureFlag$(
|
||||
FeatureFlag.UseChromiumImporter,
|
||||
);
|
||||
const client = this.system.environment.getClientType();
|
||||
const capabilities$ = combineLatest([type$, browserEnabled$]).pipe(
|
||||
map(([type, enabled]) => {
|
||||
let loaders = availableLoaders(type, client);
|
||||
if (!enabled) {
|
||||
loaders = loaders?.filter((loader) => loader !== Loader.chromium);
|
||||
}
|
||||
|
||||
const capabilities: ImporterMetadata = { type, loaders };
|
||||
if (type in Importers) {
|
||||
capabilities.instructions = Importers[type].instructions;
|
||||
}
|
||||
|
||||
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
|
||||
|
||||
return capabilities;
|
||||
}),
|
||||
);
|
||||
|
||||
return capabilities$;
|
||||
}
|
||||
|
||||
async import(
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
@@ -260,6 +294,7 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
case "chromecsv":
|
||||
case "operacsv":
|
||||
case "vivaldicsv":
|
||||
case "bravecsv":
|
||||
return new ChromeCsvImporter();
|
||||
case "firefoxcsv":
|
||||
return new FirefoxCsvImporter();
|
||||
|
||||
60
libs/importer/src/util.spec.ts
Normal file
60
libs/importer/src/util.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
import { Loader } from "./metadata";
|
||||
import { availableLoaders } from "./util";
|
||||
|
||||
describe("availableLoaders", () => {
|
||||
describe("given valid import types", () => {
|
||||
it("returns available loaders when client supports all loaders", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||
});
|
||||
|
||||
it("returns filtered loaders when client supports some loaders", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns single loader for import types with one loader", () => {
|
||||
const result = availableLoaders("chromecsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns all supported loaders for multi-loader import types", () => {
|
||||
const result = availableLoaders("bravecsv", ClientType.Desktop);
|
||||
|
||||
expect(result).toEqual([Loader.file, Loader.chromium]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("given unknown import types", () => {
|
||||
it("returns undefined when import type is not found in metadata", () => {
|
||||
const result = availableLoaders("nonexistent" as any, ClientType.Desktop);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("given different client types", () => {
|
||||
it("returns appropriate loaders for Browser client", () => {
|
||||
const result = availableLoaders("operacsv", ClientType.Browser);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns appropriate loaders for Web client", () => {
|
||||
const result = availableLoaders("chromecsv", ClientType.Web);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
|
||||
it("returns appropriate loaders for CLI client", () => {
|
||||
const result = availableLoaders("vivaldicsv", ClientType.Cli);
|
||||
|
||||
expect(result).toEqual([Loader.file]);
|
||||
});
|
||||
});
|
||||
});
|
||||
19
libs/importer/src/util.ts
Normal file
19
libs/importer/src/util.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
import { LoaderAvailability, Importers } from "./metadata";
|
||||
import { ImportType } from "./models";
|
||||
|
||||
/** Lookup the loaders supported by a specific client.
|
||||
* WARNING: this method does not supply metadata for every import type.
|
||||
* @returns `undefined` when metadata is not defined for the type, or
|
||||
* an array identifying the supported clients.
|
||||
*/
|
||||
export function availableLoaders(type: ImportType, client: ClientType) {
|
||||
if (!(type in Importers)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const capabilities = Importers[type]?.loaders ?? [];
|
||||
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
|
||||
return available;
|
||||
}
|
||||
Reference in New Issue
Block a user