mirror of
https://github.com/bitwarden/browser
synced 2026-01-04 09:33:27 +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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user