mirror of
https://github.com/bitwarden/browser
synced 2025-12-12 22:33:35 +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,
|
UsernameGenerationServiceAbstraction,
|
||||||
} from "@bitwarden/generator-legacy";
|
} from "@bitwarden/generator-legacy";
|
||||||
import {
|
import {
|
||||||
|
DefaultImportMetadataService,
|
||||||
ImportApiService,
|
ImportApiService,
|
||||||
ImportApiServiceAbstraction,
|
ImportApiServiceAbstraction,
|
||||||
|
ImportMetadataServiceAbstraction,
|
||||||
ImportService,
|
ImportService,
|
||||||
ImportServiceAbstraction,
|
ImportServiceAbstraction,
|
||||||
} from "@bitwarden/importer-core";
|
} from "@bitwarden/importer-core";
|
||||||
@@ -368,6 +370,7 @@ export default class MainBackground {
|
|||||||
authService: AuthServiceAbstraction;
|
authService: AuthServiceAbstraction;
|
||||||
loginEmailService: LoginEmailServiceAbstraction;
|
loginEmailService: LoginEmailServiceAbstraction;
|
||||||
importApiService: ImportApiServiceAbstraction;
|
importApiService: ImportApiServiceAbstraction;
|
||||||
|
importMetadataService: ImportMetadataServiceAbstraction;
|
||||||
importService: ImportServiceAbstraction;
|
importService: ImportServiceAbstraction;
|
||||||
exportApiService: VaultExportApiService;
|
exportApiService: VaultExportApiService;
|
||||||
exportService: VaultExportServiceAbstraction;
|
exportService: VaultExportServiceAbstraction;
|
||||||
@@ -1088,6 +1091,18 @@ export default class MainBackground {
|
|||||||
|
|
||||||
this.importApiService = new ImportApiService(this.apiService);
|
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.importService = new ImportService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
@@ -1099,15 +1114,6 @@ export default class MainBackground {
|
|||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.restrictedItemTypesService,
|
this.restrictedItemTypesService,
|
||||||
createSystemServiceProvider(
|
|
||||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
|
||||||
this.stateProvider,
|
|
||||||
this.policyService,
|
|
||||||
buildExtensionRegistry(),
|
|
||||||
this.logService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.configService,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualVaultExportService = new IndividualVaultExportService(
|
this.individualVaultExportService = new IndividualVaultExportService(
|
||||||
|
|||||||
@@ -4,7 +4,16 @@ import { Router } from "@angular/router";
|
|||||||
|
|
||||||
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
import { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
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 { PopOutComponent } from "../../../../platform/popup/components/pop-out.component";
|
||||||
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
import { PopupFooterComponent } from "../../../../platform/popup/layout/popup-footer.component";
|
||||||
@@ -25,6 +34,14 @@ import { PopupPageComponent } from "../../../../platform/popup/layout/popup-page
|
|||||||
PopupHeaderComponent,
|
PopupHeaderComponent,
|
||||||
PopOutComponent,
|
PopOutComponent,
|
||||||
],
|
],
|
||||||
|
providers: [
|
||||||
|
...ImporterProviders,
|
||||||
|
safeProvider({
|
||||||
|
provide: ImportMetadataServiceAbstraction,
|
||||||
|
useClass: DefaultImportMetadataService,
|
||||||
|
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ImportBrowserV2Component {
|
export class ImportBrowserV2Component {
|
||||||
protected disabled = false;
|
protected disabled = false;
|
||||||
|
|||||||
@@ -151,8 +151,10 @@ import {
|
|||||||
PasswordGenerationServiceAbstraction,
|
PasswordGenerationServiceAbstraction,
|
||||||
} from "@bitwarden/generator-legacy";
|
} from "@bitwarden/generator-legacy";
|
||||||
import {
|
import {
|
||||||
|
DefaultImportMetadataService,
|
||||||
ImportApiService,
|
ImportApiService,
|
||||||
ImportApiServiceAbstraction,
|
ImportApiServiceAbstraction,
|
||||||
|
ImportMetadataServiceAbstraction,
|
||||||
ImportService,
|
ImportService,
|
||||||
ImportServiceAbstraction,
|
ImportServiceAbstraction,
|
||||||
} from "@bitwarden/importer-core";
|
} from "@bitwarden/importer-core";
|
||||||
@@ -252,6 +254,7 @@ export class ServiceContainer {
|
|||||||
auditService: AuditService;
|
auditService: AuditService;
|
||||||
importService: ImportServiceAbstraction;
|
importService: ImportServiceAbstraction;
|
||||||
importApiService: ImportApiServiceAbstraction;
|
importApiService: ImportApiServiceAbstraction;
|
||||||
|
importMetadataService: ImportMetadataServiceAbstraction;
|
||||||
exportService: VaultExportServiceAbstraction;
|
exportService: VaultExportServiceAbstraction;
|
||||||
vaultExportApiService: VaultExportApiService;
|
vaultExportApiService: VaultExportApiService;
|
||||||
individualExportService: IndividualVaultExportServiceAbstraction;
|
individualExportService: IndividualVaultExportServiceAbstraction;
|
||||||
@@ -845,6 +848,18 @@ export class ServiceContainer {
|
|||||||
|
|
||||||
this.importApiService = new ImportApiService(this.apiService);
|
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.importService = new ImportService(
|
||||||
this.cipherService,
|
this.cipherService,
|
||||||
this.folderService,
|
this.folderService,
|
||||||
@@ -856,15 +871,6 @@ export class ServiceContainer {
|
|||||||
this.pinService,
|
this.pinService,
|
||||||
this.accountService,
|
this.accountService,
|
||||||
this.restrictedItemTypesService,
|
this.restrictedItemTypesService,
|
||||||
createSystemServiceProvider(
|
|
||||||
new KeyServiceLegacyEncryptorProvider(this.encryptService, this.keyService),
|
|
||||||
this.stateProvider,
|
|
||||||
this.policyService,
|
|
||||||
buildExtensionRegistry(),
|
|
||||||
this.logService,
|
|
||||||
this.platformUtilsService,
|
|
||||||
this.configService,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
this.individualExportService = new IndividualVaultExportService(
|
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",
|
"cbc",
|
||||||
"hex",
|
"hex",
|
||||||
"homedir",
|
"homedir",
|
||||||
|
"napi",
|
||||||
|
"napi-derive",
|
||||||
"oo7",
|
"oo7",
|
||||||
"pbkdf2",
|
"pbkdf2",
|
||||||
"rand 0.9.1",
|
"rand 0.9.1",
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ base64 = { workspace = true }
|
|||||||
cbc = { workspace = true, features = ["alloc"] }
|
cbc = { workspace = true, features = ["alloc"] }
|
||||||
hex = { workspace = true }
|
hex = { workspace = true }
|
||||||
homedir = { workspace = true }
|
homedir = { workspace = true }
|
||||||
|
napi = { workspace = true }
|
||||||
|
napi-derive = { workspace = true }
|
||||||
pbkdf2 = "=0.12.2"
|
pbkdf2 = "=0.12.2"
|
||||||
rand = { workspace = true }
|
rand = { workspace = true }
|
||||||
rusqlite = { version = "=0.37.0", features = ["bundled"] }
|
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 = "linux", path = "linux.rs")]
|
||||||
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
#[cfg_attr(target_os = "windows", path = "windows.rs")]
|
||||||
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
#[cfg_attr(target_os = "macos", path = "macos.rs")]
|
||||||
mod platform;
|
pub mod platform;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Public API
|
// Public API
|
||||||
@@ -50,8 +50,15 @@ pub enum LoginImportResult {
|
|||||||
Failure(LoginImportFailure),
|
Failure(LoginImportFailure),
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make thus async
|
pub trait InstalledBrowserRetriever {
|
||||||
pub fn get_installed_browsers() -> Result<Vec<String>> {
|
fn get_installed_browsers() -> Result<Vec<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());
|
let mut browsers = Vec::with_capacity(SUPPORTED_BROWSER_MAP.len());
|
||||||
|
|
||||||
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
for (browser, config) in SUPPORTED_BROWSER_MAP.iter() {
|
||||||
@@ -62,6 +69,7 @@ pub fn get_installed_browsers() -> Result<Vec<String>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(browsers)
|
Ok(browsers)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Make thus async
|
// TODO: Make thus async
|
||||||
@@ -104,13 +112,13 @@ pub async fn import_logins(
|
|||||||
// Private
|
// Private
|
||||||
//
|
//
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct BrowserConfig {
|
pub struct BrowserConfig {
|
||||||
name: &'static str,
|
pub name: &'static str,
|
||||||
data_dir: &'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>,
|
std::collections::HashMap<&'static str, &'static BrowserConfig>,
|
||||||
> = LazyLock::new(|| {
|
> = LazyLock::new(|| {
|
||||||
platform::SUPPORTED_BROWSERS
|
platform::SUPPORTED_BROWSERS
|
||||||
@@ -132,12 +140,12 @@ fn get_browser_data_dir(config: &BrowserConfig) -> Result<PathBuf> {
|
|||||||
//
|
//
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
trait CryptoService: Send {
|
pub trait CryptoService: Send {
|
||||||
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
async fn decrypt_to_string(&mut self, encrypted: &[u8]) -> Result<String>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(serde::Deserialize, Clone)]
|
#[derive(serde::Deserialize, Clone)]
|
||||||
struct LocalState {
|
pub struct LocalState {
|
||||||
profile: AllProfiles,
|
profile: AllProfiles,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
os_crypt: Option<OsCrypt>,
|
os_crypt: Option<OsCrypt>,
|
||||||
|
|||||||
@@ -1 +1,8 @@
|
|||||||
|
#[macro_use]
|
||||||
|
extern crate napi_derive;
|
||||||
|
|
||||||
pub mod chromium;
|
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};
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
mod util;
|
use crate::util;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Public API
|
// Public API
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use security_framework::passwords::get_generic_password;
|
|||||||
|
|
||||||
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
mod util;
|
use crate::util;
|
||||||
|
|
||||||
//
|
//
|
||||||
// Public API
|
// 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};
|
use crate::chromium::{BrowserConfig, CryptoService, LocalState};
|
||||||
|
|
||||||
#[allow(dead_code)]
|
use crate::util;
|
||||||
mod util;
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Public API
|
// Public API
|
||||||
//
|
//
|
||||||
|
|
||||||
pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
// IMPORTANT adjust array size when enabling / disabling chromium importers here
|
||||||
BrowserConfig {
|
pub const SUPPORTED_BROWSERS: [BrowserConfig; 4] = [
|
||||||
name: "Chrome",
|
|
||||||
data_dir: "AppData/Local/Google/Chrome/User Data",
|
|
||||||
},
|
|
||||||
BrowserConfig {
|
BrowserConfig {
|
||||||
name: "Chromium",
|
name: "Chromium",
|
||||||
data_dir: "AppData/Local/Chromium/User Data",
|
data_dir: "AppData/Local/Chromium/User Data",
|
||||||
@@ -29,10 +25,6 @@ pub const SUPPORTED_BROWSERS: [BrowserConfig; 6] = [
|
|||||||
name: "Microsoft Edge",
|
name: "Microsoft Edge",
|
||||||
data_dir: "AppData/Local/Microsoft/Edge/User Data",
|
data_dir: "AppData/Local/Microsoft/Edge/User Data",
|
||||||
},
|
},
|
||||||
BrowserConfig {
|
|
||||||
name: "Brave",
|
|
||||||
data_dir: "AppData/Local/BraveSoftware/Brave-Browser/User Data",
|
|
||||||
},
|
|
||||||
BrowserConfig {
|
BrowserConfig {
|
||||||
name: "Opera",
|
name: "Opera",
|
||||||
data_dir: "AppData/Roaming/Opera Software/Opera Stable",
|
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 */
|
/* 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 {
|
export declare namespace passwords {
|
||||||
/** The error message returned when a password is not found during retrieval or deletion. */
|
/** The error message returned when a password is not found during retrieval or deletion. */
|
||||||
export const PASSWORD_NOT_FOUND: string
|
export const PASSWORD_NOT_FOUND: string
|
||||||
@@ -228,6 +237,8 @@ export declare namespace chromium_importer {
|
|||||||
login?: Login
|
login?: Login
|
||||||
failure?: LoginImportFailure
|
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 getInstalledBrowsers(): Array<string>
|
||||||
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
export function getAvailableProfiles(browser: string): Array<ProfileInfo>
|
||||||
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
export function importLogins(browser: string, profileId: string): Promise<Array<LoginImportResult>>
|
||||||
|
|||||||
@@ -944,8 +944,12 @@ pub mod logging {
|
|||||||
|
|
||||||
#[napi]
|
#[napi]
|
||||||
pub mod chromium_importer {
|
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::LoginImportResult as _LoginImportResult;
|
||||||
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
use bitwarden_chromium_importer::chromium::ProfileInfo as _ProfileInfo;
|
||||||
|
use bitwarden_chromium_importer::metadata::NativeImporterMetadata;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[napi(object)]
|
#[napi(object)]
|
||||||
pub struct ProfileInfo {
|
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]
|
#[napi]
|
||||||
pub fn get_installed_browsers() -> napi::Result<Vec<String>> {
|
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()))
|
.map_err(|e| napi::Error::from_reason(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ import { chromium_importer } from "@bitwarden/desktop-napi";
|
|||||||
|
|
||||||
export class ChromiumImporterService {
|
export class ChromiumImporterService {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
ipcMain.handle("chromium_importer.getMetadata", async (event) => {
|
||||||
|
return await chromium_importer.getMetadata();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => {
|
ipcMain.handle("chromium_importer.getInstalledBrowsers", async (event) => {
|
||||||
return await chromium_importer.getInstalledBrowsers();
|
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 { JslibModule } from "@bitwarden/angular/jslib.module";
|
||||||
import { DialogRef, AsyncActionsModule, ButtonModule, DialogModule } from "@bitwarden/components";
|
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({
|
@Component({
|
||||||
templateUrl: "import-desktop.component.html",
|
templateUrl: "import-desktop.component.html",
|
||||||
@@ -15,6 +23,14 @@ import { ImportComponent } from "@bitwarden/importer-ui";
|
|||||||
ButtonModule,
|
ButtonModule,
|
||||||
ImportComponent,
|
ImportComponent,
|
||||||
],
|
],
|
||||||
|
providers: [
|
||||||
|
...ImporterProviders,
|
||||||
|
safeProvider({
|
||||||
|
provide: ImportMetadataServiceAbstraction,
|
||||||
|
useClass: DesktopImportMetadataService,
|
||||||
|
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ImportDesktopComponent {
|
export class ImportDesktopComponent {
|
||||||
protected disabled = false;
|
protected disabled = false;
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { ipcRenderer } from "electron";
|
import { ipcRenderer } from "electron";
|
||||||
|
|
||||||
|
import type { NativeImporterMetadata } from "@bitwarden/desktop-napi";
|
||||||
|
|
||||||
const chromiumImporter = {
|
const chromiumImporter = {
|
||||||
|
getMetadata: (): Promise<Record<string, NativeImporterMetadata>> =>
|
||||||
|
ipcRenderer.invoke("chromium_importer.getMetadata"),
|
||||||
getInstalledBrowsers: (): Promise<string[]> =>
|
getInstalledBrowsers: (): Promise<string[]> =>
|
||||||
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
ipcRenderer.invoke("chromium_importer.getInstalledBrowsers"),
|
||||||
getAvailableProfiles: (browser: string): Promise<any[]> =>
|
getAvailableProfiles: (browser: string): Promise<any[]> =>
|
||||||
|
|||||||
@@ -1,7 +1,16 @@
|
|||||||
import { Component } from "@angular/core";
|
import { Component } from "@angular/core";
|
||||||
import { Router } from "@angular/router";
|
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 { HeaderModule } from "../../layouts/header/header.module";
|
||||||
import { SharedModule } from "../../shared";
|
import { SharedModule } from "../../shared";
|
||||||
@@ -9,6 +18,14 @@ import { SharedModule } from "../../shared";
|
|||||||
@Component({
|
@Component({
|
||||||
templateUrl: "import-web.component.html",
|
templateUrl: "import-web.component.html",
|
||||||
imports: [SharedModule, ImportComponent, HeaderModule],
|
imports: [SharedModule, ImportComponent, HeaderModule],
|
||||||
|
providers: [
|
||||||
|
...ImporterProviders,
|
||||||
|
safeProvider({
|
||||||
|
provide: ImportMetadataServiceAbstraction,
|
||||||
|
useClass: DefaultImportMetadataService,
|
||||||
|
deps: [SYSTEM_SERVICE_PROVIDER],
|
||||||
|
}),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class ImportWebComponent {
|
export class ImportWebComponent {
|
||||||
protected loading = false;
|
protected loading = false;
|
||||||
|
|||||||
@@ -72,7 +72,11 @@ import {
|
|||||||
|
|
||||||
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
import { ImporterMetadata, DataLoader, Loader, Instructions } from "../metadata";
|
||||||
import { ImportOption, ImportResult, ImportType } from "../models";
|
import { ImportOption, ImportResult, ImportType } from "../models";
|
||||||
import { ImportCollectionServiceAbstraction, ImportServiceAbstraction } from "../services";
|
import {
|
||||||
|
ImportCollectionServiceAbstraction,
|
||||||
|
ImportMetadataServiceAbstraction,
|
||||||
|
ImportServiceAbstraction,
|
||||||
|
} from "../services";
|
||||||
|
|
||||||
import { ImportChromeComponent } from "./chrome";
|
import { ImportChromeComponent } from "./chrome";
|
||||||
import {
|
import {
|
||||||
@@ -236,6 +240,7 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
protected accountService: AccountService,
|
protected accountService: AccountService,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
private destroyRef: DestroyRef,
|
private destroyRef: DestroyRef,
|
||||||
|
protected importMetadataService: ImportMetadataServiceAbstraction,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
protected get importBlockedByPolicy(): boolean {
|
protected get importBlockedByPolicy(): boolean {
|
||||||
@@ -254,9 +259,11 @@ export class ImportComponent implements OnInit, OnDestroy, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async ngOnInit() {
|
async ngOnInit() {
|
||||||
|
await this.importMetadataService.init();
|
||||||
|
|
||||||
this.setImportOptions();
|
this.setImportOptions();
|
||||||
|
|
||||||
this.importService
|
this.importMetadataService
|
||||||
.metadata$(this.formGroup.controls.format.valueChanges)
|
.metadata$(this.formGroup.controls.format.valueChanges)
|
||||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
.subscribe({
|
.subscribe({
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ import {
|
|||||||
|
|
||||||
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
|
// FIXME: unify with `SYSTEM_SERVICE_PROVIDER` when migrating it from the generator component module
|
||||||
// to a general 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 */
|
/** Import service factories */
|
||||||
export const ImporterProviders: SafeProvider[] = [
|
export const ImporterProviders: SafeProvider[] = [
|
||||||
@@ -85,7 +87,6 @@ export const ImporterProviders: SafeProvider[] = [
|
|||||||
PinServiceAbstraction,
|
PinServiceAbstraction,
|
||||||
AccountService,
|
AccountService,
|
||||||
RestrictedItemTypesService,
|
RestrictedItemTypesService,
|
||||||
SYSTEM_SERVICE_PROVIDER,
|
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from "./dialog";
|
export * from "./dialog";
|
||||||
|
export * from "./importer-providers";
|
||||||
|
|
||||||
export { ImportComponent } from "./import.component";
|
export { ImportComponent } from "./import.component";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./models";
|
export * from "./models";
|
||||||
|
export * from "./metadata";
|
||||||
export * from "./services";
|
export * from "./services";
|
||||||
|
|
||||||
export { Importer } from "./importers/importer";
|
export { Importer } from "./importers/importer";
|
||||||
|
|||||||
@@ -2,26 +2,32 @@ import { deepFreeze } from "@bitwarden/common/tools/util";
|
|||||||
|
|
||||||
import { ImportType } from "../models";
|
import { ImportType } from "../models";
|
||||||
|
|
||||||
import { Loader, Instructions } from "./data";
|
import { Instructions, Loader } from "./data";
|
||||||
import { ImporterMetadata } from "./types";
|
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 = [
|
const importers = [
|
||||||
|
{ id: "bitwardenjson", loaders: [Loader.file], instructions: Instructions.unique },
|
||||||
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
|
// chromecsv import depends upon operating system, so ironically it doesn't support chromium
|
||||||
{ id: "chromecsv", loaders: [Loader.file], instructions: Instructions.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",
|
id: "vivaldicsv",
|
||||||
loaders: [Loader.file, Loader.chromium],
|
loaders: [Loader.file],
|
||||||
instructions: Instructions.chromium,
|
instructions: Instructions.chromium,
|
||||||
},
|
},
|
||||||
{ id: "bravecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
{ id: "bravecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||||
{ id: "edgecsv", loaders: [Loader.file, Loader.chromium], instructions: Instructions.chromium },
|
{ id: "edgecsv", loaders: [Loader.file], instructions: Instructions.chromium },
|
||||||
|
|
||||||
// FIXME: add other formats and remove `Partial` from export
|
// FIXME: add other formats and remove `Partial` from export
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
/** Describes which loaders are available for each import type */
|
/** 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])),
|
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
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @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.
|
// 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
|
// 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 { FolderView } from "@bitwarden/common/vault/models/view/folder.view";
|
||||||
|
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
import { ImporterMetadata } from "../metadata";
|
|
||||||
import { ImportOption, ImportType } from "../models/import-options";
|
import { ImportOption, ImportType } from "../models/import-options";
|
||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
|
|
||||||
@@ -17,9 +15,6 @@ export abstract class ImportServiceAbstraction {
|
|||||||
regularImportOptions: readonly ImportOption[];
|
regularImportOptions: readonly ImportOption[];
|
||||||
getImportOptions: () => ImportOption[];
|
getImportOptions: () => ImportOption[];
|
||||||
|
|
||||||
/** describes the features supported by a format */
|
|
||||||
metadata$: (type$: Observable<ImportType>) => Observable<ImporterMetadata>;
|
|
||||||
|
|
||||||
import: (
|
import: (
|
||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
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 { 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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
import { CollectionService, CollectionView } from "@bitwarden/admin-console/common";
|
||||||
import { ClientType } from "@bitwarden/client-type";
|
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
import { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
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 { 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 { Utils } from "@bitwarden/common/platform/misc/utils";
|
||||||
import { SystemServiceProvider } from "@bitwarden/common/tools/providers";
|
|
||||||
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
import { CollectionId, OrganizationId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
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 { BitwardenPasswordProtectedImporter } from "../importers/bitwarden/bitwarden-password-protected-importer";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
import { ImporterMetadata, Instructions, Loader } from "../metadata";
|
|
||||||
import { ImportType } from "../models";
|
|
||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
|
|
||||||
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
import { ImportApiServiceAbstraction } from "./import-api.service.abstraction";
|
||||||
@@ -44,7 +35,6 @@ describe("ImportService", () => {
|
|||||||
let pinService: MockProxy<PinServiceAbstraction>;
|
let pinService: MockProxy<PinServiceAbstraction>;
|
||||||
let accountService: MockProxy<AccountService>;
|
let accountService: MockProxy<AccountService>;
|
||||||
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
let restrictedItemTypesService: MockProxy<RestrictedItemTypesService>;
|
||||||
let systemServiceProvider: MockProxy<SystemServiceProvider>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
cipherService = mock<CipherService>();
|
cipherService = mock<CipherService>();
|
||||||
@@ -57,18 +47,6 @@ describe("ImportService", () => {
|
|||||||
pinService = mock<PinServiceAbstraction>();
|
pinService = mock<PinServiceAbstraction>();
|
||||||
restrictedItemTypesService = mock<RestrictedItemTypesService>();
|
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(
|
importService = new ImportService(
|
||||||
cipherService,
|
cipherService,
|
||||||
folderService,
|
folderService,
|
||||||
@@ -80,7 +58,6 @@ describe("ImportService", () => {
|
|||||||
pinService,
|
pinService,
|
||||||
accountService,
|
accountService,
|
||||||
restrictedItemTypesService,
|
restrictedItemTypesService,
|
||||||
systemServiceProvider,
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -268,136 +245,6 @@ describe("ImportService", () => {
|
|||||||
expect(importResult.folderRelationships[1]).toEqual([0, 1]);
|
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> = {}) {
|
function createCipher(options: Partial<CipherView> = {}) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
// FIXME: Update this file to be type safe and remove this and next line
|
// FIXME: Update this file to be type safe and remove this and next line
|
||||||
// @ts-strict-ignore
|
// @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.
|
// 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
|
// eslint-disable-next-line no-restricted-imports
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
} from "@bitwarden/admin-console/common";
|
} from "@bitwarden/admin-console/common";
|
||||||
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";
|
||||||
import { getUserId } from "@bitwarden/common/auth/services/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 { EncryptService } from "@bitwarden/common/key-management/crypto/abstractions/encrypt.service";
|
||||||
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
import { PinServiceAbstraction } from "@bitwarden/common/key-management/pin/pin.service.abstraction";
|
||||||
import { ImportCiphersRequest } from "@bitwarden/common/models/request/import-ciphers.request";
|
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 { ErrorResponse } from "@bitwarden/common/models/response/error.response";
|
||||||
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
import { I18nService } from "@bitwarden/common/platform/abstractions/i18n.service";
|
||||||
import { Utils } from "@bitwarden/common/platform/misc/utils";
|
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 { OrganizationId, UserId } from "@bitwarden/common/types/guid";
|
||||||
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
import { CipherService } from "@bitwarden/common/vault/abstractions/cipher.service";
|
||||||
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
import { FolderService } from "@bitwarden/common/vault/abstractions/folder/folder.service.abstraction";
|
||||||
@@ -98,7 +95,6 @@ import {
|
|||||||
PasswordDepot17XmlImporter,
|
PasswordDepot17XmlImporter,
|
||||||
} from "../importers";
|
} from "../importers";
|
||||||
import { Importer } from "../importers/importer";
|
import { Importer } from "../importers/importer";
|
||||||
import { ImporterMetadata, Importers, Loader } from "../metadata";
|
|
||||||
import {
|
import {
|
||||||
featuredImportOptions,
|
featuredImportOptions,
|
||||||
ImportOption,
|
ImportOption,
|
||||||
@@ -108,15 +104,12 @@ import {
|
|||||||
import { ImportResult } from "../models/import-result";
|
import { ImportResult } from "../models/import-result";
|
||||||
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
import { ImportApiServiceAbstraction } from "../services/import-api.service.abstraction";
|
||||||
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
import { ImportServiceAbstraction } from "../services/import.service.abstraction";
|
||||||
import { availableLoaders as availableLoaders } from "../util";
|
|
||||||
|
|
||||||
export class ImportService implements ImportServiceAbstraction {
|
export class ImportService implements ImportServiceAbstraction {
|
||||||
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
featuredImportOptions = featuredImportOptions as readonly ImportOption[];
|
||||||
|
|
||||||
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
regularImportOptions = regularImportOptions as readonly ImportOption[];
|
||||||
|
|
||||||
private logger: SemanticLogger;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private cipherService: CipherService,
|
private cipherService: CipherService,
|
||||||
private folderService: FolderService,
|
private folderService: FolderService,
|
||||||
@@ -128,55 +121,12 @@ export class ImportService implements ImportServiceAbstraction {
|
|||||||
private pinService: PinServiceAbstraction,
|
private pinService: PinServiceAbstraction,
|
||||||
private accountService: AccountService,
|
private accountService: AccountService,
|
||||||
private restrictedItemTypesService: RestrictedItemTypesService,
|
private restrictedItemTypesService: RestrictedItemTypesService,
|
||||||
private system: SystemServiceProvider,
|
) {}
|
||||||
) {
|
|
||||||
this.logger = system.log({ type: "ImportService" });
|
|
||||||
}
|
|
||||||
|
|
||||||
getImportOptions(): ImportOption[] {
|
getImportOptions(): ImportOption[] {
|
||||||
return this.featuredImportOptions.concat(this.regularImportOptions);
|
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(
|
async import(
|
||||||
importer: Importer,
|
importer: Importer,
|
||||||
fileContents: string,
|
fileContents: string,
|
||||||
|
|||||||
@@ -4,4 +4,7 @@ export { ImportApiService } from "./import-api.service";
|
|||||||
export { ImportServiceAbstraction } from "./import.service.abstraction";
|
export { ImportServiceAbstraction } from "./import.service.abstraction";
|
||||||
export { ImportService } from "./import.service";
|
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";
|
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 { ClientType } from "@bitwarden/client-type";
|
||||||
|
|
||||||
import { LoaderAvailability, Importers } from "./metadata";
|
import { LoaderAvailability, ImportersMetadata } from "./metadata";
|
||||||
import { ImportType } from "./models";
|
import { ImportType } from "./models";
|
||||||
|
|
||||||
/** Lookup the loaders supported by a specific client.
|
/** 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
|
* @returns `undefined` when metadata is not defined for the type, or
|
||||||
* an array identifying the supported clients.
|
* an array identifying the supported clients.
|
||||||
*/
|
*/
|
||||||
export function availableLoaders(type: ImportType, client: ClientType) {
|
export function availableLoaders(
|
||||||
if (!(type in Importers)) {
|
importersMetadata: ImportersMetadata,
|
||||||
|
type: ImportType,
|
||||||
|
client: ClientType,
|
||||||
|
) {
|
||||||
|
if (!importersMetadata) {
|
||||||
return undefined;
|
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));
|
const available = capabilities.filter((loader) => LoaderAvailability[loader].includes(client));
|
||||||
return available;
|
return available;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user