mirror of
https://github.com/bitwarden/browser
synced 2025-12-10 21:33:27 +00:00
[PM-25521] Move importer metadata to native code (#16695)
* Add importer metadata to native code * Impl napi code in ts * Impl napi code in ts * Fix clippy * Fix clippy * remove ts util tests * Check for installed browsers * PR fixes * test fix * fix clippy * fix tests * Bug fix * clippy fix * Correct tests * fix clippy * fix clippy * Correct tests * Correct tests * [PM-25521] Wire up loading metadata on desktop (#16813) * Initial commit * Fix issues regarding now unused feature flag * Fixed ts-strict issues --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: adudek-bw <adudek@bitwarden.com> * Remove logic to skip Brave as that now happens via the native code * Define default capabilities which can be overwritten by specifc client/platform * Fix DI issues * Do not overwrite existing importers, just add new ones or update existing ones * feat: [PM-25521] return metadata directly (not as JSON) (#16882) * feat: return metadata directly (not as JSON) * Fix broken builds Move getMetaData into chromium_importer Remove chromium_importer_metadata and any related service Parse object from native instead of json * Run cargo fmt * Fix cargo dependency sort order * Use exposed type from NAPI instead of redefining it. * Run cargo fmt --------- Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> * Only enable chromium loader for installed and supported browsers --------- Co-authored-by: Daniel James Smith <2670567+djsmith85@users.noreply.github.com> Co-authored-by: Daniel James Smith <djsmith85@users.noreply.github.com> Co-authored-by: Andreas Coroiu <acoroiu@bitwarden.com>
This commit is contained in:
@@ -221,8 +221,10 @@ import {
|
||||
UsernameGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
DefaultImportMetadataService,
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportMetadataServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "@bitwarden/importer-core";
|
||||
@@ -368,6 +370,7 @@ export default class MainBackground {
|
||||
authService: AuthServiceAbstraction;
|
||||
loginEmailService: LoginEmailServiceAbstraction;
|
||||
importApiService: ImportApiServiceAbstraction;
|
||||
importMetadataService: ImportMetadataServiceAbstraction;
|
||||
importService: ImportServiceAbstraction;
|
||||
exportApiService: VaultExportApiService;
|
||||
exportService: VaultExportServiceAbstraction;
|
||||
@@ -1088,6 +1091,18 @@ export default class MainBackground {
|
||||
|
||||
this.importApiService = new ImportApiService(this.apiService);
|
||||
|
||||
this.importMetadataService = new DefaultImportMetadataService(
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.importService = new ImportService(
|
||||
this.cipherService,
|
||||
this.folderService,
|
||||
@@ -1099,15 +1114,6 @@ export default class MainBackground {
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.restrictedItemTypesService,
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.individualVaultExportService = new IndividualVaultExportService(
|
||||
|
||||
@@ -4,7 +4,16 @@ import { Router } from "@angular/router";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
import { ImportComponent } from "@bitwarden/importer-ui";
|
||||
import {
|
||||
DefaultImportMetadataService,
|
||||
ImportMetadataServiceAbstraction,
|
||||
} from "@bitwarden/importer-core";
|
||||
import {
|
||||
ImportComponent,
|
||||
ImporterProviders,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
} from "@bitwarden/importer-ui";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
import { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||
@@ -25,6 +34,14 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
||||
PopupHeaderComponent,
|
||||
PopOutComponent,
|
||||
],
|
||||
providers: [
|
||||
...ImporterProviders,
|
||||
safeProvider({
|
||||
provide: ImportMetadataServiceAbstraction,
|
||||
useClass: DefaultImportMetadataService,
|
||||
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ImportBrowserV2Component {
|
||||
protected disabled = false;
|
||||
|
||||
@@ -151,8 +151,10 @@ import {
|
||||
PasswordGenerationServiceAbstraction,
|
||||
} from "@bitwarden/generator-legacy";
|
||||
import {
|
||||
DefaultImportMetadataService,
|
||||
ImportApiService,
|
||||
ImportApiServiceAbstraction,
|
||||
ImportMetadataServiceAbstraction,
|
||||
ImportService,
|
||||
ImportServiceAbstraction,
|
||||
} from "@bitwarden/importer-core";
|
||||
@@ -252,6 +254,7 @@ export class ServiceContainer {
|
||||
auditService: AuditService;
|
||||
importService: ImportServiceAbstraction;
|
||||
importApiService: ImportApiServiceAbstraction;
|
||||
importMetadataService: ImportMetadataServiceAbstraction;
|
||||
exportService: VaultExportServiceAbstraction;
|
||||
vaultExportApiService: VaultExportApiService;
|
||||
individualExportService: IndividualVaultExportServiceAbstraction;
|
||||
@@ -845,6 +848,18 @@ export class ServiceContainer {
|
||||
|
||||
this.importApiService = new ImportApiService(this.apiService);
|
||||
|
||||
this.importMetadataService = new DefaultImportMetadataService(
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.importService = new ImportService(
|
||||
this.cipherService,
|
||||
this.folderService,
|
||||
@@ -856,15 +871,6 @@ export class ServiceContainer {
|
||||
this.pinService,
|
||||
this.accountService,
|
||||
this.restrictedItemTypesService,
|
||||
createSystemServiceProvider(
|
||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
||||
this.stateProvider,
|
||||
this.policyService,
|
||||
buildExtensionRegistry(),
|
||||
this.logService,
|
||||
this.platformUtilsService,
|
||||
this.configService,
|
||||
),
|
||||
);
|
||||
|
||||
this.individualExportService = new IndividualVaultExportService(
|
||||
|
||||
2
apps/desktop/desktop_native/Cargo.lock
generated
2
apps/desktop/desktop_native/Cargo.lock
generated
@@ -450,6 +450,8 @@ dependencies = [
|
||||
"cbc",
|
||||
"hex",
|
||||
"homedir",
|
||||
"napi",
|
||||
"napi-derive",
|
||||
"oo7",
|
||||
"pbkdf2",
|
||||
"rand 0.9.1",
|
||||
|
||||
@@ -14,6 +14,8 @@ base64 = { workspace = true }
|
||||
cbc = { workspace = true, features = ["alloc"] }
|
||||
hex = { workspace = true }
|
||||
homedir = { workspace = true }
|
||||
napi = { workspace = true }
|
||||
napi-derive = { workspace = true }
|
||||
pbkdf2 = "=0.12.2"
|
||||
rand = { workspace = true }
|
||||
rusqlite = { version = "=0.37.0", features = ["bundled"] }
|
||||
|
||||
@@ -11,7 +11,7 @@ use rusqlite::{params, Connection};
|
||||
#[cfg_attr(target_os = "linux", path = "linux.rs")]
|
||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||
mod platform;
|
||||
pub mod platform;
|
||||
|
||||
//
|
||||
// Public API
|
||||
@@ -50,18 +50,26 @@ pub enum LoginImportResult {
|
||||
Failure(LoginImportFailure),
|
||||
}
|
||||
|
||||
// TODO: Make thus async
|
||||
pub fn get_installed_browsers() -> Result<Vec<String>> {
|
||||
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||
pub trait InstalledBrowserRetriever {
|
||||
fn get_installed_browsers() -> Result<Vec<String>>;
|
||||
}
|
||||
|
||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||
let data_dir = get_browser_data_dir(config)?;
|
||||
if data_dir.exists() {
|
||||
browsers.push((*browser).to_string());
|
||||
pub struct DefaultInstalledBrowserRetriever {}
|
||||
|
||||
impl InstalledBrowserRetriever for DefaultInstalledBrowserRetriever {
|
||||
// TODO: Make thus async
|
||||
fn get_installed_browsers() -> Result<Vec<String>> {
|
||||
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||
|
||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||
let data_dir = get_browser_data_dir(config)?;
|
||||
if data_dir.exists() {
|
||||
browsers.push((*browser).to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(browsers)
|
||||
Ok(browsers)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Make thus async
|
||||
@@ -104,13 +112,13 @@ pub async fn import_logins(
|
||||
// Private
|
||||
//
|
||||
|
||||
#[derive(Debug)]
|
||||
struct BrowserConfig {
|
||||
name: &'static str,
|
||||
data_dir: &'static str,
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct BrowserConfig {
|
||||
pub name: &'static str,
|
||||
pub data_dir: &'static str,
|
||||
}
|
||||
|
||||
static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
pub static SUPPORTED_BROWSER_MAP: LazyLock<
|
||||
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||
> = LazyLock::new(|| {
|
||||
platform::SUPPORTED_BROWSERS
|
||||
@@ -132,12 +140,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
||||
//
|
||||
|
||||
#[async_trait]
|
||||
trait CryptoService: Send {
|
||||
pub trait CryptoService: Send {
|
||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Clone)]
|
||||
struct LocalState {
|
||||
pub struct LocalState {
|
||||
profile: AllProfiles,
|
||||
#[allow(dead_code)]
|
||||
os_crypt: Option<OsCrypt>,
|
||||
|
||||
@@ -1 +1,8 @@
|
||||
#[macro_use]
|
||||
extern crate napi_derive;
|
||||
|
||||
pub mod chromium;
|
||||
pub mod metadata;
|
||||
pub mod util;
|
||||
|
||||
pub use crate::chromium::platform::SUPPORTED_BROWSERS as PLATFORM_SUPPORTED_BROWSERS;
|
||||
|
||||
@@ -6,7 +6,7 @@ use oo7::XDG_SCHEMA_ATTRIBUTE;
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
mod util;
|
||||
use crate::util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
|
||||
@@ -4,7 +4,7 @@ use security_framework::passwords::get_generic_password;
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
mod util;
|
||||
use crate::util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use crate::{chromium::InstalledBrowserRetriever, PLATFORM_SUPPORTED_BROWSERS};
|
||||
|
||||
#[napi(object)]
|
||||
/// Mechanisms that load data into the importer
|
||||
pub struct NativeImporterMetadata {
|
||||
/// Identifies the importer
|
||||
pub id: String,
|
||||
/// Describes the strategies used to obtain imported data
|
||||
pub loaders: Vec<&'static str>,
|
||||
/// Identifies the instructions for the importer
|
||||
pub instructions: &'static str,
|
||||
}
|
||||
|
||||
/// Returns a map of supported importers based on the current platform.
|
||||
///
|
||||
/// Only browsers listed in PLATFORM_SUPPORTED_BROWSERS will have the "chromium" loader.
|
||||
/// All importers will have the "file" loader.
|
||||
pub fn get_supported_importers<T: InstalledBrowserRetriever>(
|
||||
) -> HashMap<String, NativeImporterMetadata> {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
// Check for installed browsers
|
||||
let installed_browsers = T::get_installed_browsers().unwrap_or_default();
|
||||
|
||||
const IMPORTERS: [(&str, &str); 6] = [
|
||||
("chromecsv", "Chrome"),
|
||||
("chromiumcsv", "Chromium"),
|
||||
("bravecsv", "Brave"),
|
||||
("operacsv", "Opera"),
|
||||
("vivaldicsv", "Vivaldi"),
|
||||
("edgecsv", "Microsoft Edge"),
|
||||
];
|
||||
|
||||
let supported: HashSet<&'static str> =
|
||||
PLATFORM_SUPPORTED_BROWSERS.iter().map(|b| b.name).collect();
|
||||
|
||||
for (id, browser_name) in IMPORTERS {
|
||||
let mut loaders: Vec<&'static str> = vec!["file"];
|
||||
if supported.contains(browser_name) {
|
||||
loaders.push("chromium");
|
||||
}
|
||||
|
||||
if installed_browsers.contains(&browser_name.to_string()) {
|
||||
map.insert(
|
||||
id.to_string(),
|
||||
NativeImporterMetadata {
|
||||
id: id.to_string(),
|
||||
loaders,
|
||||
instructions: "chromium",
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
/*
|
||||
Tests are cfg-gated based upon OS, and must be compiled/run on each OS for full coverage
|
||||
*/
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::chromium::{InstalledBrowserRetriever, SUPPORTED_BROWSER_MAP};
|
||||
|
||||
pub struct MockInstalledBrowserRetriever {}
|
||||
|
||||
impl InstalledBrowserRetriever for MockInstalledBrowserRetriever {
|
||||
fn get_installed_browsers() -> Result<Vec<String>, anyhow::Error> {
|
||||
Ok(SUPPORTED_BROWSER_MAP
|
||||
.keys()
|
||||
.map(|browser| browser.to_string())
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn map_keys(map: &HashMap<String, NativeImporterMetadata>) -> HashSet<String> {
|
||||
map.keys().cloned().collect()
|
||||
}
|
||||
|
||||
fn get_loaders(
|
||||
map: &HashMap<String, NativeImporterMetadata>,
|
||||
id: &str,
|
||||
) -> HashSet<&'static str> {
|
||||
map.get(id)
|
||||
.map(|m| m.loaders.iter().copied().collect::<HashSet<_>>())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"chromecsv".to_string(),
|
||||
"chromiumcsv".to_string(),
|
||||
"bravecsv".to_string(),
|
||||
"operacsv".to_string(),
|
||||
"vivaldicsv".to_string(),
|
||||
"edgecsv".to_string(),
|
||||
]);
|
||||
assert_eq!(map.len(), expected.len());
|
||||
assert_eq!(map_keys(&map), expected);
|
||||
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.iter().any(|l| *l == "file"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[test]
|
||||
fn macos_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let ids = [
|
||||
"chromecsv",
|
||||
"chromiumcsv",
|
||||
"bravecsv",
|
||||
"operacsv",
|
||||
"vivaldicsv",
|
||||
"edgecsv",
|
||||
];
|
||||
for id in ids {
|
||||
let loaders = get_loaders(&map, id);
|
||||
assert!(loaders.contains("file"));
|
||||
assert!(loaders.contains("chromium"), "missing chromium for {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"chromecsv".to_string(),
|
||||
"chromiumcsv".to_string(),
|
||||
"bravecsv".to_string(),
|
||||
"operacsv".to_string(),
|
||||
]);
|
||||
assert_eq!(map.len(), expected.len());
|
||||
assert_eq!(map_keys(&map), expected);
|
||||
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.iter().any(|l| *l == "file"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn linux_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let ids = ["chromecsv", "chromiumcsv", "bravecsv", "operacsv"];
|
||||
|
||||
for id in ids {
|
||||
let loaders = get_loaders(&map, id);
|
||||
assert!(loaders.contains("file"));
|
||||
assert!(loaders.contains("chromium"), "missing chromium for {id}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn returns_all_known_importers() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
|
||||
let expected: HashSet<String> = HashSet::from([
|
||||
"chromiumcsv".to_string(),
|
||||
"edgecsv".to_string(),
|
||||
"operacsv".to_string(),
|
||||
"vivaldicsv".to_string(),
|
||||
]);
|
||||
assert_eq!(map.len(), expected.len());
|
||||
assert_eq!(map_keys(&map), expected);
|
||||
|
||||
for (key, meta) in map.iter() {
|
||||
assert_eq!(&meta.id, key);
|
||||
assert_eq!(meta.instructions, "chromium");
|
||||
assert!(meta.loaders.iter().any(|l| *l == "file"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[test]
|
||||
fn windows_specific_loaders_match_const_array() {
|
||||
let map = get_supported_importers::<MockInstalledBrowserRetriever>();
|
||||
let ids = ["chromiumcsv", "edgecsv", "operacsv", "vivaldicsv"];
|
||||
|
||||
for id in ids {
|
||||
let loaders = get_loaders(&map, id);
|
||||
assert!(loaders.contains("file"));
|
||||
assert!(loaders.contains("chromium"), "missing chromium for {id}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,18 +9,14 @@ use windows::Win32::Foundation::{LocalFree, HLOCAL};
|
||||
|
||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||
|
||||
#[allow(dead_code)]
|
||||
mod util;
|
||||
use crate::util;
|
||||
|
||||
//
|
||||
// Public API
|
||||
//
|
||||
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
BrowserConfig {
|
||||
name: "Chrome",
|
||||
data_dir: "AppData/Local/Google/Chrome/User Data",
|
||||
},
|
||||
// IMPORTANT adjust array size when enabling / disabling chromium importers here
|
||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||
BrowserConfig {
|
||||
name: "Chromium",
|
||||
data_dir: "AppData/Local/Chromium/User Data",
|
||||
@@ -29,10 +25,6 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
||||
name: "Microsoft Edge",
|
||||
data_dir: "AppData/Local/Microsoft/Edge/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Brave",
|
||||
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
||||
},
|
||||
BrowserConfig {
|
||||
name: "Opera",
|
||||
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
|
||||
|
||||
11
apps/desktop/desktop_native/napi/index.d.ts
vendored
11
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -3,6 +3,15 @@
|
||||
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
/** Mechanisms that load data into the importer */
|
||||
export interface NativeImporterMetadata {
|
||||
/** Identifies the importer */
|
||||
id: string
|
||||
/** Describes the strategies used to obtain imported data */
|
||||
loaders: Array<string>
|
||||
/** Identifies the instructions for the importer */
|
||||
instructions: string
|
||||
}
|
||||
export declare namespace passwords {
|
||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||
export const PASSWORD_NOT_FOUND: string
|
||||
@@ -228,6 +237,8 @@ export declare namespace chromium_importer {
|
||||
login?: Login
|
||||
failure?: LoginImportFailure
|
||||
}
|
||||
/** Returns OS aware metadata describing supported Chromium based importers as a JSON string. */
|
||||
export function getMetadata(): Record<string, NativeImporterMetadata>
|
||||
export function getInstalledBrowsers(): Array<string>
|
||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||
|
||||
@@ -944,8 +944,12 @@ pub mod logging {
|
||||
|
||||
#[napi]
|
||||
pub mod chromium_importer {
|
||||
use bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever;
|
||||
use bitwarden_chromium_importer::chromium::InstalledBrowserRetriever;
|
||||
use bitwarden_chromium_importer::chromium::LoginImportResult as _LoginImportResult;
|
||||
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
||||
use bitwarden_chromium_importer::metadata::NativeImporterMetadata;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[napi(object)]
|
||||
pub struct ProfileInfo {
|
||||
@@ -1007,9 +1011,17 @@ pub mod chromium_importer {
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
/// Returns OS aware metadata describing supported Chromium based importers as a JSON string.
|
||||
pub fn get_metadata() -> HashMap<String, NativeImporterMetadata> {
|
||||
bitwarden_chromium_importer::metadata::get_supported_importers::<
|
||||
DefaultInstalledBrowserRetriever,
|
||||
>()
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
|
||||
bitwarden_chromium_importer::chromium::get_installed_browsers()
|
||||
bitwarden_chromium_importer::chromium::DefaultInstalledBrowserRetriever::get_installed_browsers()
|
||||
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,10 @@ import { chromium_importer } from "@bitwarden/desktop-napi";
|
||||
|
||||
export class ChromiumImporterService {
|
||||
constructor() {
|
||||
ipcMain.handle("chromium_importer.getMetadata", async (event) => {
|
||||
return await chromium_importer.getMetadata();
|
||||
});
|
||||
|
||||
ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => {
|
||||
return await chromium_importer.getInstalledBrowsers();
|
||||
});
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
|
||||
import {
|
||||
ImportType,
|
||||
DefaultImportMetadataService,
|
||||
ImportMetadataServiceAbstraction,
|
||||
DataLoader,
|
||||
ImporterMetadata,
|
||||
InstructionLink,
|
||||
Instructions,
|
||||
Loader,
|
||||
} from "@bitwarden/importer-core";
|
||||
|
||||
export class DesktopImportMetadataService
|
||||
extends DefaultImportMetadataService
|
||||
implements ImportMetadataServiceAbstraction
|
||||
{
|
||||
constructor(system: SystemServiceProvider) {
|
||||
super(system);
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
const metadata = await ipc.tools.chromiumImporter.getMetadata();
|
||||
await this.parseNativeMetaData(metadata);
|
||||
await super.init();
|
||||
}
|
||||
|
||||
private async parseNativeMetaData(raw: Record<string, NativeImporterMetadata>): Promise<void> {
|
||||
const entries = Object.entries(raw).map(([id, meta]) => {
|
||||
const loaders = meta.loaders.map(this.mapLoader);
|
||||
const instructions = this.mapInstructions(meta.instructions);
|
||||
const mapped: ImporterMetadata = {
|
||||
type: id as ImportType,
|
||||
loaders,
|
||||
...(instructions ? { instructions } : {}),
|
||||
};
|
||||
return [id, mapped] as const;
|
||||
});
|
||||
|
||||
// Do not overwrite existing importers, just add new ones or update existing ones
|
||||
this.importers = {
|
||||
...this.importers,
|
||||
...Object.fromEntries(entries),
|
||||
};
|
||||
}
|
||||
|
||||
private mapLoader(name: string): DataLoader {
|
||||
switch (name) {
|
||||
case "file":
|
||||
return Loader.file;
|
||||
case "chromium":
|
||||
return Loader.chromium;
|
||||
default:
|
||||
throw new Error(`Unknown loader from native module: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
private mapInstructions(name: string): InstructionLink | undefined {
|
||||
switch (name) {
|
||||
case "chromium":
|
||||
return Instructions.chromium;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,15 @@ import { Component } from "@angular/core";
|
||||
|
||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||
import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
||||
import { ImportComponent } from "@bitwarden/importer-ui";
|
||||
import { ImportMetadataServiceAbstraction } from "@bitwarden/importer-core";
|
||||
import {
|
||||
ImportComponent,
|
||||
ImporterProviders,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
} from "@bitwarden/importer-ui";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
import { DesktopImportMetadataService } from "./desktop-import-metadata.service";
|
||||
|
||||
@Component({
|
||||
templateUrl: "import-desktop.component.html",
|
||||
@@ -15,6 +23,14 @@ import { ImportComponent } from "@bitwarden/importer-ui";
|
||||
ButtonModule,
|
||||
ImportComponent,
|
||||
],
|
||||
providers: [
|
||||
...ImporterProviders,
|
||||
safeProvider({
|
||||
provide: ImportMetadataServiceAbstraction,
|
||||
useClass: DesktopImportMetadataService,
|
||||
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ImportDesktopComponent {
|
||||
protected disabled = false;
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { ipcRenderer } from "electron";
|
||||
|
||||
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
|
||||
|
||||
const chromiumImporter = {
|
||||
getMetadata: (): Promise<Record<string, NativeImporterMetadata>> =>
|
||||
ipcRenderer.invoke("chromium_importer.getMetadata"),
|
||||
getInstalledBrowsers: (): Promise<string[]> =>
|
||||
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
||||
getAvailableProfiles: (browser: string): Promise<any[]> =>
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
import { Component } from "@angular/core";
|
||||
import { Router } from "@angular/router";
|
||||
|
||||
import { ImportComponent } from "@bitwarden/importer-ui";
|
||||
import {
|
||||
DefaultImportMetadataService,
|
||||
ImportMetadataServiceAbstraction,
|
||||
} from "@bitwarden/importer-core";
|
||||
import {
|
||||
ImportComponent,
|
||||
ImporterProviders,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
} from "@bitwarden/importer-ui";
|
||||
import { safeProvider } from "@bitwarden/ui-common";
|
||||
|
||||
import { HeaderModule } from "../../layouts/header/header.module";
|
||||
import { SharedModule } from "../../shared";
|
||||
@@ -9,6 +18,14 @@ import { SharedModule } from "../../shared";
|
||||
@Component({
|
||||
templateUrl: "import-web.component.html",
|
||||
imports: [SharedModule, ImportComponent, HeaderModule],
|
||||
providers: [
|
||||
...ImporterProviders,
|
||||
safeProvider({
|
||||
provide: ImportMetadataServiceAbstraction,
|
||||
useClass: DefaultImportMetadataService,
|
||||
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||
}),
|
||||
],
|
||||
})
|
||||
export class ImportWebComponent {
|
||||
protected loading = false;
|
||||
|
||||
@@ -72,7 +72,11 @@ import {
|
||||
|
||||
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
||||
import { ImportOption, ImportResult, ImportType } from "../models";
|
||||
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
|
||||
import {
|
||||
ImportCollectionServiceAbstraction,
|
||||
ImportMetadataServiceAbstraction,
|
||||
ImportServiceAbstraction,
|
||||
} from "../services";
|
||||
|
||||
import { ImportChromeComponent } from "./chrome";
|
||||
import {
|
||||
@@ -236,6 +240,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
protected accountService: AccountService,
|
||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||
private destroyRef: DestroyRef,
|
||||
protected importMetadataService: ImportMetadataServiceAbstraction,
|
||||
) {}
|
||||
|
||||
protected get importBlockedByPolicy(): boolean {
|
||||
@@ -254,9 +259,11 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.importMetadataService.init();
|
||||
|
||||
this.setImportOptions();
|
||||
|
||||
this.importService
|
||||
this.importMetadataService
|
||||
.metadata$(this.formGroup.controls.format.valueChanges)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
|
||||
@@ -37,7 +37,9 @@ import {
|
||||
|
||||
// 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");
|
||||
export const SYSTEM_SERVICE_PROVIDER = new SafeInjectionToken<SystemServiceProvider>(
|
||||
"SystemServices",
|
||||
);
|
||||
|
||||
/** Import service factories */
|
||||
export const ImporterProviders: SafeProvider[] = [
|
||||
@@ -85,7 +87,6 @@ export const ImporterProviders: SafeProvider[] = [
|
||||
PinServiceAbstraction,
|
||||
AccountService,
|
||||
RestrictedItemTypesService,
|
||||
SYSTEM_SERVICE_PROVIDER,
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./dialog";
|
||||
export * from "./importer-providers";
|
||||
|
||||
export { ImportComponent } from "./import.component";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from "./models";
|
||||
export * from "./metadata";
|
||||
export * from "./services";
|
||||
|
||||
export { Importer } from "./importers/importer";
|
||||
|
||||
@@ -2,26 +2,32 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
||||
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { Loader, Instructions } from "./data";
|
||||
import { Instructions, Loader } from "./data";
|
||||
import { ImporterMetadata } from "./types";
|
||||
|
||||
// FIXME: load this data from rust code
|
||||
export type ImportersMetadata = Partial<Record<ImportType, ImporterMetadata>>;
|
||||
|
||||
/** List of all supported importers and their default capabilities
|
||||
* Note: the loaders listed here are the ones that are supported in all clients.
|
||||
* Specific clients may have additional loaders available based on platform capabilities.
|
||||
*/
|
||||
const importers = [
|
||||
{ id: "bitwardenjson", loaders: [Loader.file], instructions: Instructions.unique },
|
||||
// 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: "operacsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{
|
||||
id: "vivaldicsv",
|
||||
loaders: [Loader.file, Loader.chromium],
|
||||
loaders: [Loader.file],
|
||||
instructions: Instructions.chromium,
|
||||
},
|
||||
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
||||
{ id: "bravecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||
{ id: "edgecsv", loaders: [Loader.file], 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(
|
||||
export const Importers: ImportersMetadata = deepFreeze(
|
||||
Object.fromEntries(importers.map((i) => [i.id, i])),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { map, Observable } from "rxjs";
|
||||
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
|
||||
import { ImporterMetadata, Importers, ImportersMetadata } from "../metadata";
|
||||
import { ImportType } from "../models/import-options";
|
||||
import { availableLoaders } from "../util";
|
||||
|
||||
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
|
||||
export class DefaultImportMetadataService implements ImportMetadataServiceAbstraction {
|
||||
protected importers: ImportersMetadata = Importers;
|
||||
private logger: SemanticLogger;
|
||||
|
||||
constructor(protected system: SystemServiceProvider) {
|
||||
this.logger = system.log({ type: "ImportMetadataService" });
|
||||
}
|
||||
|
||||
async init(): Promise<void> {
|
||||
// no-op for default implementation
|
||||
}
|
||||
|
||||
metadata$(type$: Observable<ImportType>): Observable<ImporterMetadata> {
|
||||
const client = this.system.environment.getClientType();
|
||||
const capabilities$ = type$.pipe(
|
||||
map((type) => {
|
||||
if (!this.importers) {
|
||||
return { type, loaders: [] };
|
||||
}
|
||||
|
||||
const loaders = availableLoaders(this.importers, type, client);
|
||||
|
||||
if (!loaders || loaders.length === 0) {
|
||||
return { type, loaders: [] };
|
||||
}
|
||||
|
||||
const capabilities: ImporterMetadata = { type, loaders };
|
||||
if (type in this.importers) {
|
||||
capabilities.instructions = this.importers[type]?.instructions;
|
||||
}
|
||||
|
||||
this.logger.debug({ importType: type, capabilities }, "capabilities updated");
|
||||
|
||||
return capabilities;
|
||||
}),
|
||||
);
|
||||
|
||||
return capabilities$;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
|
||||
import { ImporterMetadata } from "../metadata";
|
||||
import { ImportType } from "../models/import-options";
|
||||
|
||||
export abstract class ImportMetadataServiceAbstraction {
|
||||
abstract init(): Promise<void>;
|
||||
|
||||
/** describes the features supported by a format */
|
||||
abstract metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||
}
|
||||
110
libs/importer/src/services/import-metadata.service.spec.ts
Normal file
110
libs/importer/src/services/import-metadata.service.spec.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { mock, MockProxy } from "jest-mock-extended";
|
||||
import { Subject, firstValueFrom } from "rxjs";
|
||||
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
import { ConfigService } from "@bitwarden/common/platform/abstractions/config/config.service";
|
||||
import { PlatformUtilsService } from "@bitwarden/common/platform/abstractions/platform-utils.service";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
|
||||
import { ImporterMetadata, Instructions } from "../metadata";
|
||||
import { ImportType } from "../models";
|
||||
|
||||
import { DefaultImportMetadataService } from "./default-import-metadata.service";
|
||||
import { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
|
||||
describe("ImportMetadataService", () => {
|
||||
let sut: ImportMetadataServiceAbstraction;
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
const environment = mock<PlatformUtilsService>();
|
||||
environment.getClientType.mockReturnValue(ClientType.Desktop);
|
||||
|
||||
systemServiceProvider = mock<SystemServiceProvider>({
|
||||
configService,
|
||||
environment,
|
||||
log: jest.fn().mockReturnValue({ debug: jest.fn() }),
|
||||
});
|
||||
|
||||
sut = new DefaultImportMetadataService(systemServiceProvider);
|
||||
});
|
||||
|
||||
describe("metadata$", () => {
|
||||
let typeSubject: Subject<ImportType>;
|
||||
let mockLogger: { debug: jest.Mock };
|
||||
|
||||
beforeEach(() => {
|
||||
typeSubject = new Subject<ImportType>();
|
||||
mockLogger = { debug: jest.fn() };
|
||||
|
||||
const configService = mock<ConfigService>();
|
||||
|
||||
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
|
||||
sut = new DefaultImportMetadataService(systemServiceProvider);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
typeSubject.complete();
|
||||
});
|
||||
|
||||
it("should emit metadata when type$ emits", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(sut.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 update when type$ changes", async () => {
|
||||
const emissions: ImporterMetadata[] = [];
|
||||
const subscription = sut.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 log debug information with correct data", async () => {
|
||||
const testType: ImportType = "chromecsv";
|
||||
|
||||
const metadataPromise = firstValueFrom(sut.metadata$(typeSubject));
|
||||
typeSubject.next(testType);
|
||||
|
||||
await metadataPromise;
|
||||
|
||||
expect(mockLogger.debug).toHaveBeenCalledWith(
|
||||
{ importType: testType, capabilities: expect.any(Object) },
|
||||
"capabilities updated",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,5 @@
|
||||
// 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
|
||||
@@ -8,7 +7,6 @@ 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";
|
||||
|
||||
@@ -17,9 +15,6 @@ export abstract class ImportServiceAbstraction {
|
||||
regularImportOptions: readonly ImportOption[];
|
||||
getImportOptions: () => ImportOption[];
|
||||
|
||||
/** describes the features supported by a format */
|
||||
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
||||
|
||||
import: (
|
||||
importer: Importer,
|
||||
fileContents: string,
|
||||
|
||||
@@ -1,20 +1,13 @@
|
||||
// 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 { 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";
|
||||
@@ -25,8 +18,6 @@ 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";
|
||||
@@ -44,7 +35,6 @@ describe("ImportService", () => {
|
||||
let pinService: MockProxy<PinServiceAbstraction>;
|
||||
let accountService: MockProxy<AccountService>;
|
||||
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
||||
|
||||
beforeEach(() => {
|
||||
cipherService = mock<CipherService>();
|
||||
@@ -57,18 +47,6 @@ describe("ImportService", () => {
|
||||
pinService = mock<PinServiceAbstraction>();
|
||||
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,
|
||||
@@ -80,7 +58,6 @@ describe("ImportService", () => {
|
||||
pinService,
|
||||
accountService,
|
||||
restrictedItemTypesService,
|
||||
systemServiceProvider,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -268,136 +245,6 @@ 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 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 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 { combineLatest, firstValueFrom, map, Observable } from "rxjs";
|
||||
import { firstValueFrom, map } 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
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from "@bitwarden/admin-console/common";
|
||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||
import { getUserId } from "@bitwarden/common/auth/services/account.service";
|
||||
import { DeviceType } 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 { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
||||
@@ -20,8 +19,6 @@ 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||
import { SemanticLogger } from "@bitwarden/common/tools/log";
|
||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
||||
import { OrganizationId, UserId } 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";
|
||||
@@ -98,7 +95,6 @@ import {
|
||||
PasswordDepot17XmlImporter,
|
||||
} from "../importers";
|
||||
import { Importer } from "../importers/importer";
|
||||
import { ImporterMetadata, Importers, Loader } from "../metadata";
|
||||
import {
|
||||
featuredImportOptions,
|
||||
ImportOption,
|
||||
@@ -108,15 +104,12 @@ 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,
|
||||
@@ -128,55 +121,12 @@ export class ImportService implements ImportServiceAbstraction {
|
||||
private pinService: PinServiceAbstraction,
|
||||
private accountService: AccountService,
|
||||
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 client = this.system.environment.getClientType();
|
||||
const capabilities$ = combineLatest([type$]).pipe(
|
||||
map(([type]) => {
|
||||
let loaders = availableLoaders(type, client);
|
||||
|
||||
// Mac App Store is currently disabled due to sandboxing.
|
||||
let isUnsupported = this.system.environment.isMacAppStore();
|
||||
|
||||
// disable the chromium loader for Brave on Windows only
|
||||
if (type === "bravecsv") {
|
||||
try {
|
||||
const device = this.system.environment.getDevice();
|
||||
const isWindowsDesktop = device === DeviceType.WindowsDesktop;
|
||||
if (isWindowsDesktop) {
|
||||
isUnsupported = true;
|
||||
}
|
||||
} catch {
|
||||
isUnsupported = true;
|
||||
}
|
||||
}
|
||||
// If the browser is unsupported, remove the chromium loader
|
||||
if (isUnsupported) {
|
||||
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,
|
||||
|
||||
@@ -4,4 +4,7 @@ export { ImportApiService } from "./import-api.service";
|
||||
export { ImportServiceAbstraction } from "./import.service.abstraction";
|
||||
export { ImportService } from "./import.service";
|
||||
|
||||
export { ImportMetadataServiceAbstraction } from "./import-metadata.service.abstraction";
|
||||
export { DefaultImportMetadataService } from "./default-import-metadata.service";
|
||||
|
||||
export { ImportCollectionServiceAbstraction } from "./import-collection.service.abstraction";
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ClientType } from "@bitwarden/client-type";
|
||||
|
||||
import { LoaderAvailability, Importers } from "./metadata";
|
||||
import { LoaderAvailability, ImportersMetadata } from "./metadata";
|
||||
import { ImportType } from "./models";
|
||||
|
||||
/** Lookup the loaders supported by a specific client.
|
||||
@@ -8,12 +8,20 @@ import { ImportType } from "./models";
|
||||
* @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)) {
|
||||
export function availableLoaders(
|
||||
importersMetadata: ImportersMetadata,
|
||||
type: ImportType,
|
||||
client: ClientType,
|
||||
) {
|
||||
if (!importersMetadata) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const capabilities = Importers[type]?.loaders ?? [];
|
||||
if (!(type in importersMetadata)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const capabilities = importersMetadata[type]?.loaders ?? [];
|
||||
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
|
||||
return available;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user