From eb7dc301bfaddf28d26e5e3e6f09f8af9f4af384 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 11:23:16 +0200 Subject: [PATCH 01/28] Add ClientManagedTokens trait --- crates/bitwarden-core/src/auth/renew.rs | 18 +++++--- crates/bitwarden-core/src/client/client.rs | 18 ++++++-- crates/bitwarden-core/src/client/internal.rs | 42 ++++++++----------- .../src/platform/get_user_api_key.rs | 12 ++---- 4 files changed, 49 insertions(+), 41 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index 7b3e6f02c..a4fdb030f 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -10,24 +10,30 @@ use crate::{ }; use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, - client::{internal::InternalClient, LoginMethod, UserLoginMethod}, + client::{ + internal::{InternalClient, Tokens}, + LoginMethod, UserLoginMethod, + }, NotAuthenticatedError, }; pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60; - let tokens = client - .tokens - .read() - .expect("RwLock is not poisoned") - .clone(); let login_method = client .login_method .read() .expect("RwLock is not poisoned") .clone(); + let tokens = { + let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); + match &*tokens_guard { + Tokens::SdkManaged(tokens) => tokens.clone(), + _ => return Err(NotAuthenticatedError.into()), + } + }; + if let (Some(expires), Some(login_method)) = (tokens.expires_on, login_method) { if Utc::now().timestamp() < expires - TOKEN_RENEW_MARGIN_SECONDS { return Ok(()); diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index cf22ffef4..a9bb28bff 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -10,7 +10,7 @@ use super::internal::InternalClient; use crate::client::flags::Flags; use crate::client::{ client_settings::ClientSettings, - internal::{ApiConfigurations, Tokens}, + internal::{ApiConfigurations, ClientManagedTokens, SdkManagedTokens, Tokens}, }; /// The main struct to interact with the Bitwarden SDK. @@ -26,7 +26,19 @@ pub struct Client { impl Client { #[allow(missing_docs)] - pub fn new(settings_input: Option) -> Self { + pub fn new(settings: Option) -> Self { + Self::new_tokens(settings, Tokens::SdkManaged(SdkManagedTokens::default())) + } + + #[allow(missing_docs)] + pub fn new_with_client_tokens( + settings: Option, + tokens: Box, + ) -> Self { + Self::new_tokens(settings, Tokens::ClientManaged(tokens)) + } + + fn new_tokens(settings_input: Option, tokens: Tokens) -> Self { let settings = settings_input.unwrap_or_default(); fn new_client_builder() -> reqwest::ClientBuilder { @@ -81,7 +93,7 @@ impl Client { Self { internal: Arc::new(InternalClient { user_id: OnceLock::new(), - tokens: RwLock::new(Tokens::default()), + tokens: RwLock::new(tokens), login_method: RwLock::new(None), #[cfg(feature = "internal")] flags: RwLock::new(Flags::default()), diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 9cc39bd08..12fcbae5e 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -33,8 +33,19 @@ pub struct ApiConfigurations { pub device_type: DeviceType, } +#[derive(Debug)] +pub(crate) enum Tokens { + SdkManaged(SdkManagedTokens), + ClientManaged(Box), +} + +pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { + /// Returns the access token, if available. + fn get_access_token(&self) -> Option; +} + #[derive(Debug, Default, Clone)] -pub(crate) struct Tokens { +pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read // from the secrets manager SDK. #[cfg_attr(not(feature = "internal"), allow(dead_code))] @@ -117,11 +128,12 @@ impl InternalClient { } pub(crate) fn set_tokens(&self, token: String, refresh_token: Option, expires_in: u64) { - *self.tokens.write().expect("RwLock is not poisoned") = Tokens { - access_token: Some(token.clone()), - expires_on: Some(Utc::now().timestamp() + expires_in as i64), - refresh_token, - }; + *self.tokens.write().expect("RwLock is not poisoned") = + Tokens::SdkManaged(SdkManagedTokens { + access_token: Some(token.clone()), + expires_on: Some(Utc::now().timestamp() + expires_in as i64), + refresh_token, + }); let mut guard = self .__api_configurations .write() @@ -132,24 +144,6 @@ impl InternalClient { inner.api.oauth_access_token = Some(token); } - #[allow(missing_docs)] - #[cfg(feature = "internal")] - pub fn is_authed(&self) -> bool { - let is_token_set = self - .tokens - .read() - .expect("RwLock is not poisoned") - .access_token - .is_some(); - let is_login_method_set = self - .login_method - .read() - .expect("RwLock is not poisoned") - .is_some(); - - is_token_set || is_login_method_set - } - #[allow(missing_docs)] #[cfg(feature = "internal")] pub fn get_kdf(&self) -> Result { diff --git a/crates/bitwarden-core/src/platform/get_user_api_key.rs b/crates/bitwarden-core/src/platform/get_user_api_key.rs index a127abab6..24c89c0d5 100644 --- a/crates/bitwarden-core/src/platform/get_user_api_key.rs +++ b/crates/bitwarden-core/src/platform/get_user_api_key.rs @@ -63,14 +63,10 @@ pub(crate) async fn get_user_api_key( } fn get_login_method(client: &Client) -> Result, NotAuthenticatedError> { - if client.internal.is_authed() { - client - .internal - .get_login_method() - .ok_or(NotAuthenticatedError) - } else { - Err(NotAuthenticatedError) - } + client + .internal + .get_login_method() + .ok_or(NotAuthenticatedError) } /// Build the secret verification request. From a58679cf1609ce9d38dac0bdc1be4799b14c9c6f Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 11:58:55 +0200 Subject: [PATCH 02/28] wip --- crates/bitwarden-core/Cargo.toml | 1 + crates/bitwarden-core/src/auth/renew.rs | 37 ++++-- crates/bitwarden-core/src/client/client.rs | 2 +- crates/bitwarden-core/src/client/internal.rs | 12 +- crates/bitwarden-wasm-internal/Cargo.toml | 4 + crates/bitwarden-wasm-internal/src/client.rs | 129 ++++++++++++++++++- 6 files changed, 169 insertions(+), 16 deletions(-) diff --git a/crates/bitwarden-core/Cargo.toml b/crates/bitwarden-core/Cargo.toml index c99b4918d..170fa1dd1 100644 --- a/crates/bitwarden-core/Cargo.toml +++ b/crates/bitwarden-core/Cargo.toml @@ -29,6 +29,7 @@ wasm = [ ] # WASM support [dependencies] +async-trait = { workspace = true } base64 = ">=0.22.1, <0.23" bitwarden-api-api = { workspace = true } bitwarden-api-identity = { workspace = true } diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index a4fdb030f..1143580c0 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -11,13 +11,40 @@ use crate::{ use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, client::{ - internal::{InternalClient, Tokens}, + internal::{InternalClient, SdkManagedTokens, Tokens}, LoginMethod, UserLoginMethod, }, NotAuthenticatedError, }; +// TODO: Clean up, the match is ugly pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { + let tokens = { + let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); + match &*tokens_guard { + Tokens::SdkManaged(tokens) => (Some(tokens.clone()), None), + Tokens::ClientManaged(tokens) => (None, Some(tokens.clone())), + } + }; + + match tokens { + (Some(tokens), None) => renew_token_sdk_managed(client, tokens).await, + (None, Some(tokens)) => { + let token = tokens + .get_access_token() + .await + .ok_or(NotAuthenticatedError)?; + client.set_tokens_internal(token); + Ok(()) + } + _ => Err(NotAuthenticatedError.into()), + } +} + +pub(crate) async fn renew_token_sdk_managed( + client: &InternalClient, + tokens: SdkManagedTokens, +) -> Result<(), LoginError> { const TOKEN_RENEW_MARGIN_SECONDS: i64 = 5 * 60; let login_method = client @@ -26,14 +53,6 @@ pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginErro .expect("RwLock is not poisoned") .clone(); - let tokens = { - let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); - match &*tokens_guard { - Tokens::SdkManaged(tokens) => tokens.clone(), - _ => return Err(NotAuthenticatedError.into()), - } - }; - if let (Some(expires), Some(login_method)) = (tokens.expires_on, login_method) { if Utc::now().timestamp() < expires - TOKEN_RENEW_MARGIN_SECONDS { return Ok(()); diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index a9bb28bff..d1f5ccf1a 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -33,7 +33,7 @@ impl Client { #[allow(missing_docs)] pub fn new_with_client_tokens( settings: Option, - tokens: Box, + tokens: Arc, ) -> Self { Self::new_tokens(settings, Tokens::ClientManaged(tokens)) } diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 12fcbae5e..a12eecd85 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -36,19 +36,20 @@ pub struct ApiConfigurations { #[derive(Debug)] pub(crate) enum Tokens { SdkManaged(SdkManagedTokens), - ClientManaged(Box), + ClientManaged(Arc), } +#[async_trait::async_trait] pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { /// Returns the access token, if available. - fn get_access_token(&self) -> Option; + async fn get_access_token(&self) -> Option; } #[derive(Debug, Default, Clone)] pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read // from the secrets manager SDK. - #[cfg_attr(not(feature = "internal"), allow(dead_code))] + #[allow(dead_code)] access_token: Option, pub(crate) expires_on: Option, @@ -134,6 +135,11 @@ impl InternalClient { expires_on: Some(Utc::now().timestamp() + expires_in as i64), refresh_token, }); + self.set_tokens_internal(token); + } + + /// Used to set tokens for internal API clients, use [set_tokens] for SdkManagedTokens. + pub(crate) fn set_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations .write() diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index db4d673ea..32c11787b 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -30,6 +30,7 @@ bitwarden-threading = { workspace = true } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } +js-sys = "0.3" log = "0.4.20" serde = { workspace = true } tsify-next = { workspace = true } @@ -37,5 +38,8 @@ tsify-next = { workspace = true } wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" +[dev-dependencies] +wasm-bindgen-test = "0.3" + [lints] workspace = true diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 200ff69ff..2ded5bbac 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,5 +1,5 @@ extern crate console_error_panic_hook; -use std::fmt::Display; +use std::{fmt::Display, sync::Arc}; use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; use bitwarden_error::bitwarden_error; @@ -18,8 +18,9 @@ pub struct BitwardenClient(pub(crate) Client); impl BitwardenClient { #[allow(missing_docs)] #[wasm_bindgen(constructor)] - pub fn new(settings: Option) -> Self { - Self(Client::new(settings)) + pub fn new(settings: Option, token_provider: JsTokenProvider) -> Self { + let tokens = Arc::new(WasmClientManagedTokens::new(token_provider)); + Self(Client::new_with_client_tokens(settings, tokens)) } /// Test method, echoes back the input @@ -79,3 +80,125 @@ impl Display for TestError { write!(f, "{}", self.0) } } + +/// JavaScript-compatible token provider using function closure +#[wasm_bindgen] +pub struct JsTokenProvider { + get_access_token_fn: js_sys::Function, +} + +impl std::fmt::Debug for JsTokenProvider { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("JsTokenProvider") + .field("get_access_token_fn", &"") + .finish() + } +} + +#[wasm_bindgen] +impl JsTokenProvider { + #[wasm_bindgen(constructor)] + pub fn new(get_access_token_fn: js_sys::Function) -> Self { + Self { + get_access_token_fn, + } + } +} + +/// Wrapper to make JsTokenProvider compatible with ClientManagedTokens +#[derive(Debug)] +struct WasmClientManagedTokens { + js_provider: JsTokenProvider, +} + +impl WasmClientManagedTokens { + fn new(js_provider: JsTokenProvider) -> Self { + Self { js_provider } + } +} + +impl bitwarden_core::client::internal::ClientManagedTokens for WasmClientManagedTokens { + fn get_access_token(&self) -> Option { + match self + .js_provider + .get_access_token_fn + .call0(&wasm_bindgen::JsValue::UNDEFINED) + { + Ok(result) => { + if result.is_null() || result.is_undefined() { + None + } else { + result.as_string() + } + } + Err(_) => None, + } + } +} + +// SAFETY: JsTokenProvider is only used in WASM context where there's no real threading +unsafe impl Send for WasmClientManagedTokens {} +unsafe impl Sync for WasmClientManagedTokens {} + +#[cfg(test)] +#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` +mod tests { + use super::*; + use bitwarden_core::client::internal::ClientManagedTokens; + use wasm_bindgen_test::*; + + // Note: These tests are designed to run in a WASM environment + // Run with: wasm-pack test --node + + #[wasm_bindgen_test] + fn test_js_token_provider_creation() { + // Create a simple function that returns a test token + let js_fn = js_sys::Function::new_no_args("return 'test-token-123';"); + let provider = JsTokenProvider::new(js_fn); + + // Verify the provider was created successfully + // This mainly tests the constructor works without panicking + assert!(format!("{:?}", provider).contains("JsTokenProvider")); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_valid_token() { + let js_fn = js_sys::Function::new_no_args("return 'valid-access-token';"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, Some("valid-access-token".to_string())); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_null_token() { + let js_fn = js_sys::Function::new_no_args("return null;"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, None); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_undefined_token() { + let js_fn = js_sys::Function::new_no_args("return undefined;"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + assert_eq!(result, None); + } + + #[wasm_bindgen_test] + fn test_wasm_client_managed_tokens_with_error() { + let js_fn = js_sys::Function::new_no_args("throw new Error('Token error');"); + let provider = JsTokenProvider::new(js_fn); + let tokens = WasmClientManagedTokens::new(provider); + + let result = tokens.get_access_token(); + // Should return None when the JS function throws an error + assert_eq!(result, None); + } +} From 425aaa07fec38425a3ba72407913e50ad993f769 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:11:36 +0200 Subject: [PATCH 03/28] Fix --- Cargo.lock | 3 + crates/bitwarden-wasm-internal/src/client.rs | 81 +++++++++----------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d42405358..c74a57a51 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -357,6 +357,7 @@ dependencies = [ name = "bitwarden-core" version = "1.0.0" dependencies = [ + "async-trait", "base64", "bitwarden-api-api", "bitwarden-api-identity", @@ -726,11 +727,13 @@ dependencies = [ "bitwarden-vault", "console_error_panic_hook", "console_log", + "js-sys", "log", "serde", "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", + "wasm-bindgen-test", ] [[package]] diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index 2ded5bbac..d60a4720b 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,10 +1,13 @@ extern crate console_error_panic_hook; use std::{fmt::Display, sync::Arc}; -use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; +use bitwarden_core::{ + client::internal::ClientManagedTokens, key_management::CryptoClient, Client, ClientSettings, +}; use bitwarden_error::bitwarden_error; use bitwarden_exporters::ExporterClientExt; use bitwarden_generators::GeneratorClientsExt; +use bitwarden_threading::ThreadBoundRunner; use bitwarden_vault::{VaultClient, VaultClientExt}; use wasm_bindgen::prelude::*; @@ -81,65 +84,50 @@ impl Display for TestError { } } -/// JavaScript-compatible token provider using function closure -#[wasm_bindgen] -pub struct JsTokenProvider { - get_access_token_fn: js_sys::Function, -} - -impl std::fmt::Debug for JsTokenProvider { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("JsTokenProvider") - .field("get_access_token_fn", &"") - .finish() - } +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" +export interface TokenProvider { + get_access_token(): Promise; } +"#; #[wasm_bindgen] -impl JsTokenProvider { - #[wasm_bindgen(constructor)] - pub fn new(get_access_token_fn: js_sys::Function) -> Self { - Self { - get_access_token_fn, - } - } -} +extern "C" { + #[wasm_bindgen(js_name = TokenProvider)] + pub type JsTokenProvider; -/// Wrapper to make JsTokenProvider compatible with ClientManagedTokens -#[derive(Debug)] -struct WasmClientManagedTokens { - js_provider: JsTokenProvider, + #[wasm_bindgen(method)] + pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; } +struct WasmClientManagedTokens(ThreadBoundRunner); + impl WasmClientManagedTokens { - fn new(js_provider: JsTokenProvider) -> Self { - Self { js_provider } + pub fn new(js_provider: JsTokenProvider) -> Self { + Self(ThreadBoundRunner::new(js_provider)) } } -impl bitwarden_core::client::internal::ClientManagedTokens for WasmClientManagedTokens { - fn get_access_token(&self) -> Option { - match self - .js_provider - .get_access_token_fn - .call0(&wasm_bindgen::JsValue::UNDEFINED) - { - Ok(result) => { - if result.is_null() || result.is_undefined() { - None - } else { - result.as_string() - } - } - Err(_) => None, - } +impl std::fmt::Debug for WasmClientManagedTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasmClientManagedTokens").finish() } } -// SAFETY: JsTokenProvider is only used in WASM context where there's no real threading -unsafe impl Send for WasmClientManagedTokens {} -unsafe impl Sync for WasmClientManagedTokens {} +#[async_trait::async_trait] +impl ClientManagedTokens for WasmClientManagedTokens { + async fn get_access_token(&self) -> Option { + let t = self + .0 + .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .await + .unwrap(); + + t + } +} +/* #[cfg(test)] #[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` mod tests { @@ -202,3 +190,4 @@ mod tests { assert_eq!(result, None); } } +*/ From 890450d08547c5368d524c1b1d2dd856f9cb3a1b Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:12:13 +0200 Subject: [PATCH 04/28] Migrate folder --- Cargo.lock | 1 + crates/bitwarden-vault/Cargo.toml | 4 +- crates/bitwarden-vault/src/folder.rs | 8 ++- crates/bitwarden-vault/src/folder_client.rs | 70 ++++++++++++++++++++- 4 files changed, 77 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c74a57a51..a296e11e6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index e06ce1460..d49b95274 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,7 +23,8 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify-next", - "dep:wasm-bindgen" + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] @@ -48,6 +49,7 @@ tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 18083a5f5..bc3e5ca0b 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -20,11 +20,13 @@ use crate::VaultParseError; #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Folder { - id: Option, - name: EncString, - revision_date: DateTime, + pub id: Option, + pub name: EncString, + pub revision_date: DateTime, } +bitwarden_state::register_repository_item!(Folder, "Folder"); + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 90f62bd0b..259ce025e 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,18 +1,46 @@ -use bitwarden_core::Client; +use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; +use bitwarden_core::{ApiError, Client}; +use bitwarden_error::bitwarden_error; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - Folder, FolderView, + Folder, FolderView, VaultParseError, }; +/// Request to add or edit a folder. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct FolderAddEditRequest { + pub name: String, +} + #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, } +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateFolderError { + #[error(transparent)] + Encrypt(#[from] EncryptError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { #[allow(missing_docs)] @@ -35,4 +63,42 @@ impl FoldersClient { let views = key_store.decrypt_list(&folders)?; Ok(views) } + + /// Create a new folder and save it to the server. + pub async fn create(&self, request: FolderAddEditRequest) -> Result { + // TODO: We should probably not use a Folder model here, but rather create FolderRequestModel directly? + let folder = self.encrypt(FolderView { + id: None, + name: request.name, + revision_date: Utc::now(), + })?; + + let config = self.client.internal.get_api_configurations().await; + let req = folders_api::folders_post( + &config.api, + Some(FolderRequestModel { + name: folder.name.to_string(), + }), + ) + .await + .map_err(ApiError::from)?; + + Ok(req.try_into()?) + } + + /// Edit the folder. + /// + /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. + /// TODO: Save the folder to the server and state. + pub fn edit_without_state( + &self, + old_folder: Folder, + folder: FolderAddEditRequest, + ) -> Result { + self.encrypt(FolderView { + id: old_folder.id, + name: folder.name, + revision_date: old_folder.revision_date, + }) + } } From 3813a28c261c7454a13707a8d21e8f4f1b717b68 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:24:41 +0200 Subject: [PATCH 05/28] Add repository --- crates/bitwarden-vault/src/folder.rs | 2 +- crates/bitwarden-vault/src/folder_client.rs | 21 ++++++++++++++++++--- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index bc3e5ca0b..eb342cfa3 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -15,7 +15,7 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 259ce025e..7e54589e8 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,6 +1,7 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; -use bitwarden_core::{ApiError, Client}; +use bitwarden_core::{require, ApiError, Client, MissingFieldError}; use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::RepositoryError; use chrono::Utc; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -39,6 +40,10 @@ pub enum CreateFolderError { Api(#[from] ApiError), #[error(transparent)] VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), } #[cfg_attr(feature = "wasm", wasm_bindgen)] @@ -74,7 +79,7 @@ impl FoldersClient { })?; let config = self.client.internal.get_api_configurations().await; - let req = folders_api::folders_post( + let resp = folders_api::folders_post( &config.api, Some(FolderRequestModel { name: folder.name.to_string(), @@ -83,7 +88,17 @@ impl FoldersClient { .await .map_err(ApiError::from)?; - Ok(req.try_into()?) + let folder: Folder = resp.try_into()?; + + self.client + .platform() + .state() + .get_client_managed::() + .ok_or(MissingFieldError("Folder not found in state"))? + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(folder) } /// Edit the folder. From 665a5a2267410ff8f6414f9da04b16b407730ea0 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 13:59:08 +0200 Subject: [PATCH 06/28] Cleanup --- Cargo.lock | 2 - crates/bitwarden-vault/src/folder_client.rs | 3 +- crates/bitwarden-wasm-internal/Cargo.toml | 4 - crates/bitwarden-wasm-internal/src/client.rs | 118 +----------------- .../src/platform/mod.rs | 1 + .../src/platform/token_provider.rs | 43 +++++++ 6 files changed, 51 insertions(+), 120 deletions(-) create mode 100644 crates/bitwarden-wasm-internal/src/platform/token_provider.rs diff --git a/Cargo.lock b/Cargo.lock index a296e11e6..d09378f89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -728,13 +728,11 @@ dependencies = [ "bitwarden-vault", "console_error_panic_hook", "console_log", - "js-sys", "log", "serde", "tsify-next", "wasm-bindgen", "wasm-bindgen-futures", - "wasm-bindgen-test", ] [[package]] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 7e54589e8..da7c5fd05 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -71,7 +71,8 @@ impl FoldersClient { /// Create a new folder and save it to the server. pub async fn create(&self, request: FolderAddEditRequest) -> Result { - // TODO: We should probably not use a Folder model here, but rather create FolderRequestModel directly? + // TODO: We should probably not use a Folder model here, but rather create + // FolderRequestModel directly? let folder = self.encrypt(FolderView { id: None, name: request.name, diff --git a/crates/bitwarden-wasm-internal/Cargo.toml b/crates/bitwarden-wasm-internal/Cargo.toml index 32c11787b..db4d673ea 100644 --- a/crates/bitwarden-wasm-internal/Cargo.toml +++ b/crates/bitwarden-wasm-internal/Cargo.toml @@ -30,7 +30,6 @@ bitwarden-threading = { workspace = true } bitwarden-vault = { workspace = true, features = ["wasm"] } console_error_panic_hook = "0.1.7" console_log = { version = "1.0.0", features = ["color"] } -js-sys = "0.3" log = "0.4.20" serde = { workspace = true } tsify-next = { workspace = true } @@ -38,8 +37,5 @@ tsify-next = { workspace = true } wasm-bindgen = { version = "=0.2.100", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" -[dev-dependencies] -wasm-bindgen-test = "0.3" - [lints] workspace = true diff --git a/crates/bitwarden-wasm-internal/src/client.rs b/crates/bitwarden-wasm-internal/src/client.rs index d60a4720b..bdd601bcc 100644 --- a/crates/bitwarden-wasm-internal/src/client.rs +++ b/crates/bitwarden-wasm-internal/src/client.rs @@ -1,17 +1,17 @@ extern crate console_error_panic_hook; use std::{fmt::Display, sync::Arc}; -use bitwarden_core::{ - client::internal::ClientManagedTokens, key_management::CryptoClient, Client, ClientSettings, -}; +use bitwarden_core::{key_management::CryptoClient, Client, ClientSettings}; use bitwarden_error::bitwarden_error; use bitwarden_exporters::ExporterClientExt; use bitwarden_generators::GeneratorClientsExt; -use bitwarden_threading::ThreadBoundRunner; use bitwarden_vault::{VaultClient, VaultClientExt}; use wasm_bindgen::prelude::*; -use crate::platform::PlatformClient; +use crate::platform::{ + token_provider::{JsTokenProvider, WasmClientManagedTokens}, + PlatformClient, +}; #[allow(missing_docs)] #[wasm_bindgen] @@ -83,111 +83,3 @@ impl Display for TestError { write!(f, "{}", self.0) } } - -#[wasm_bindgen(typescript_custom_section)] -const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" -export interface TokenProvider { - get_access_token(): Promise; -} -"#; - -#[wasm_bindgen] -extern "C" { - #[wasm_bindgen(js_name = TokenProvider)] - pub type JsTokenProvider; - - #[wasm_bindgen(method)] - pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; -} - -struct WasmClientManagedTokens(ThreadBoundRunner); - -impl WasmClientManagedTokens { - pub fn new(js_provider: JsTokenProvider) -> Self { - Self(ThreadBoundRunner::new(js_provider)) - } -} - -impl std::fmt::Debug for WasmClientManagedTokens { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("WasmClientManagedTokens").finish() - } -} - -#[async_trait::async_trait] -impl ClientManagedTokens for WasmClientManagedTokens { - async fn get_access_token(&self) -> Option { - let t = self - .0 - .run_in_thread(async move |c| c.get_access_token().await.as_string()) - .await - .unwrap(); - - t - } -} - -/* -#[cfg(test)] -#[allow(dead_code)] // Not actually dead, but rust-analyzer doesn't understand `wasm_bindgen_test` -mod tests { - use super::*; - use bitwarden_core::client::internal::ClientManagedTokens; - use wasm_bindgen_test::*; - - // Note: These tests are designed to run in a WASM environment - // Run with: wasm-pack test --node - - #[wasm_bindgen_test] - fn test_js_token_provider_creation() { - // Create a simple function that returns a test token - let js_fn = js_sys::Function::new_no_args("return 'test-token-123';"); - let provider = JsTokenProvider::new(js_fn); - - // Verify the provider was created successfully - // This mainly tests the constructor works without panicking - assert!(format!("{:?}", provider).contains("JsTokenProvider")); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_valid_token() { - let js_fn = js_sys::Function::new_no_args("return 'valid-access-token';"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, Some("valid-access-token".to_string())); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_null_token() { - let js_fn = js_sys::Function::new_no_args("return null;"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, None); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_undefined_token() { - let js_fn = js_sys::Function::new_no_args("return undefined;"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - assert_eq!(result, None); - } - - #[wasm_bindgen_test] - fn test_wasm_client_managed_tokens_with_error() { - let js_fn = js_sys::Function::new_no_args("throw new Error('Token error');"); - let provider = JsTokenProvider::new(js_fn); - let tokens = WasmClientManagedTokens::new(provider); - - let result = tokens.get_access_token(); - // Should return None when the JS function throws an error - assert_eq!(result, None); - } -} -*/ diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index b2b977659..1bb5b551b 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -3,6 +3,7 @@ use bitwarden_vault::Cipher; use wasm_bindgen::prelude::wasm_bindgen; mod repository; +pub mod token_provider; #[wasm_bindgen] pub struct PlatformClient(Client); diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs new file mode 100644 index 000000000..8693512f5 --- /dev/null +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -0,0 +1,43 @@ +use bitwarden_core::client::internal::ClientManagedTokens; +use bitwarden_threading::ThreadBoundRunner; +use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; + +#[wasm_bindgen(typescript_custom_section)] +const TOKEN_CUSTOM_TS_TYPE: &'static str = r#" +export interface TokenProvider { + get_access_token(): Promise; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(js_name = TokenProvider)] + pub type JsTokenProvider; + + #[wasm_bindgen(method)] + pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; +} + +pub(crate) struct WasmClientManagedTokens(ThreadBoundRunner); + +impl WasmClientManagedTokens { + pub fn new(js_provider: JsTokenProvider) -> Self { + Self(ThreadBoundRunner::new(js_provider)) + } +} + +impl std::fmt::Debug for WasmClientManagedTokens { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WasmClientManagedTokens").finish() + } +} + +#[async_trait::async_trait] +impl ClientManagedTokens for WasmClientManagedTokens { + async fn get_access_token(&self) -> Option { + self.0 + .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .await + .unwrap_or_default() + } +} From 4bcf32add352f36c1fd4e2b7a2645cd1f3e3b064 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:07:19 +0200 Subject: [PATCH 07/28] Cleanup --- crates/bitwarden-core/src/auth/renew.rs | 45 +++++++++++--------- crates/bitwarden-core/src/client/internal.rs | 5 ++- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index 1143580c0..c5b5093af 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use chrono::Utc; use super::login::LoginError; @@ -11,37 +13,38 @@ use crate::{ use crate::{ auth::api::{request::ApiTokenRequest, response::IdentityTokenResponse}, client::{ - internal::{InternalClient, SdkManagedTokens, Tokens}, + internal::{ClientManagedTokens, InternalClient, SdkManagedTokens, Tokens}, LoginMethod, UserLoginMethod, }, NotAuthenticatedError, }; -// TODO: Clean up, the match is ugly pub(crate) async fn renew_token(client: &InternalClient) -> Result<(), LoginError> { - let tokens = { - let tokens_guard = client.tokens.read().expect("RwLock is not poisoned"); - match &*tokens_guard { - Tokens::SdkManaged(tokens) => (Some(tokens.clone()), None), - Tokens::ClientManaged(tokens) => (None, Some(tokens.clone())), - } - }; + let tokens_guard = client + .tokens + .read() + .expect("RwLock is not poisoned") + .clone(); - match tokens { - (Some(tokens), None) => renew_token_sdk_managed(client, tokens).await, - (None, Some(tokens)) => { - let token = tokens - .get_access_token() - .await - .ok_or(NotAuthenticatedError)?; - client.set_tokens_internal(token); - Ok(()) - } - _ => Err(NotAuthenticatedError.into()), + match tokens_guard { + Tokens::SdkManaged(tokens) => renew_token_sdk_managed(client, tokens).await, + Tokens::ClientManaged(tokens) => renew_token_client_managed(client, tokens).await, } } -pub(crate) async fn renew_token_sdk_managed( +async fn renew_token_client_managed( + client: &InternalClient, + tokens: Arc, +) -> Result<(), LoginError> { + let token = tokens + .get_access_token() + .await + .ok_or(NotAuthenticatedError)?; + client.set_tokens_internal(token); + Ok(()) +} + +async fn renew_token_sdk_managed( client: &InternalClient, tokens: SdkManagedTokens, ) -> Result<(), LoginError> { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index a12eecd85..e253cb488 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -33,18 +33,21 @@ pub struct ApiConfigurations { pub device_type: DeviceType, } -#[derive(Debug)] +/// Access and refresh tokens used for authentication and authorization. +#[derive(Debug, Clone)] pub(crate) enum Tokens { SdkManaged(SdkManagedTokens), ClientManaged(Arc), } +/// Access tokens managed by client applications, such as the web or mobile apps. #[async_trait::async_trait] pub trait ClientManagedTokens: std::fmt::Debug + Send + Sync { /// Returns the access token, if available. async fn get_access_token(&self) -> Option; } +/// Tokens managed by the SDK, the SDK will automatically handle token renewal. #[derive(Debug, Default, Clone)] pub(crate) struct SdkManagedTokens { // These two fields are always written to, but they are not read From f05db2caef6166341fd82b4f4cb0461a2eecd259 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:25:25 +0200 Subject: [PATCH 08/28] Wire up repository and return folder view --- crates/bitwarden-vault/src/folder_client.rs | 10 ++++++++-- crates/bitwarden-wasm-internal/src/platform/mod.rs | 8 +++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index da7c5fd05..650316175 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,5 +1,6 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{require, ApiError, Client, MissingFieldError}; +use bitwarden_crypto::Decryptable; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::RepositoryError; use chrono::Utc; @@ -37,6 +38,8 @@ pub enum CreateFolderError { #[error(transparent)] Encrypt(#[from] EncryptError), #[error(transparent)] + Decrypt(#[from] DecryptError), + #[error(transparent)] Api(#[from] ApiError), #[error(transparent)] VaultParse(#[from] VaultParseError), @@ -70,7 +73,10 @@ impl FoldersClient { } /// Create a new folder and save it to the server. - pub async fn create(&self, request: FolderAddEditRequest) -> Result { + pub async fn create( + &self, + request: FolderAddEditRequest, + ) -> Result { // TODO: We should probably not use a Folder model here, but rather create // FolderRequestModel directly? let folder = self.encrypt(FolderView { @@ -99,7 +105,7 @@ impl FoldersClient { .set(require!(folder.id).to_string(), folder.clone()) .await?; - Ok(folder) + Ok(self.decrypt(folder)?) } /// Edit the folder. diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 1bb5b551b..9d6c4274c 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::Cipher; +use bitwarden_vault::{Cipher, Folder}; use wasm_bindgen::prelude::wasm_bindgen; mod repository; @@ -31,6 +31,7 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); +repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -38,4 +39,9 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } + + pub fn register_folder_repository(&self, store: FolderRepository) { + let store = store.into_channel_impl(); + self.0.platform().state().register_client_managed(store) + } } From 5261641befacd657f44b9f2889fff33270e51d61 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:42:12 +0200 Subject: [PATCH 09/28] clippt --- crates/bitwarden-vault/src/folder_client.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 650316175..55a3cc1dc 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,6 +1,5 @@ use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{require, ApiError, Client, MissingFieldError}; -use bitwarden_crypto::Decryptable; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::RepositoryError; use chrono::Utc; From 44cec30a8eb8b13a92c8d12b7efbed0c6fb2cc7f Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 30 Jun 2025 14:55:30 +0200 Subject: [PATCH 10/28] Fix linting --- crates/bitwarden-core/src/client/internal.rs | 2 +- crates/bitwarden-wasm-internal/src/platform/token_provider.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index e253cb488..b69aaf96a 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -141,7 +141,7 @@ impl InternalClient { self.set_tokens_internal(token); } - /// Used to set tokens for internal API clients, use [set_tokens] for SdkManagedTokens. + /// Used to set tokens for internal API clients, use `set_tokens` for SdkManagedTokens. pub(crate) fn set_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs index 8693512f5..2cbc67d87 100644 --- a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -36,7 +36,7 @@ impl std::fmt::Debug for WasmClientManagedTokens { impl ClientManagedTokens for WasmClientManagedTokens { async fn get_access_token(&self) -> Option { self.0 - .run_in_thread(async move |c| c.get_access_token().await.as_string()) + .run_in_thread(|c| async move { c.get_access_token().await.as_string() }) .await .unwrap_or_default() } From 2c1bdf0e294a215de845bbd4dee09be3865ed715 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:18:29 +0200 Subject: [PATCH 11/28] Remove folder client changes --- crates/bitwarden-vault/src/folder_client.rs | 91 +-------------------- 1 file changed, 2 insertions(+), 89 deletions(-) diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 55a3cc1dc..90f62bd0b 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,53 +1,18 @@ -use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; -use bitwarden_core::{require, ApiError, Client, MissingFieldError}; -use bitwarden_error::bitwarden_error; -use bitwarden_state::repository::RepositoryError; -use chrono::Utc; -use serde::{Deserialize, Serialize}; -use thiserror::Error; -#[cfg(feature = "wasm")] -use tsify_next::Tsify; +use bitwarden_core::Client; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - Folder, FolderView, VaultParseError, + Folder, FolderView, }; -/// Request to add or edit a folder. -#[derive(Serialize, Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] -#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] -pub struct FolderAddEditRequest { - pub name: String, -} - #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, } -#[allow(missing_docs)] -#[bitwarden_error(flat)] -#[derive(Debug, Error)] -pub enum CreateFolderError { - #[error(transparent)] - Encrypt(#[from] EncryptError), - #[error(transparent)] - Decrypt(#[from] DecryptError), - #[error(transparent)] - Api(#[from] ApiError), - #[error(transparent)] - VaultParse(#[from] VaultParseError), - #[error(transparent)] - MissingField(#[from] MissingFieldError), - #[error(transparent)] - RepositoryError(#[from] RepositoryError), -} - #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { #[allow(missing_docs)] @@ -70,56 +35,4 @@ impl FoldersClient { let views = key_store.decrypt_list(&folders)?; Ok(views) } - - /// Create a new folder and save it to the server. - pub async fn create( - &self, - request: FolderAddEditRequest, - ) -> Result { - // TODO: We should probably not use a Folder model here, but rather create - // FolderRequestModel directly? - let folder = self.encrypt(FolderView { - id: None, - name: request.name, - revision_date: Utc::now(), - })?; - - let config = self.client.internal.get_api_configurations().await; - let resp = folders_api::folders_post( - &config.api, - Some(FolderRequestModel { - name: folder.name.to_string(), - }), - ) - .await - .map_err(ApiError::from)?; - - let folder: Folder = resp.try_into()?; - - self.client - .platform() - .state() - .get_client_managed::() - .ok_or(MissingFieldError("Folder not found in state"))? - .set(require!(folder.id).to_string(), folder.clone()) - .await?; - - Ok(self.decrypt(folder)?) - } - - /// Edit the folder. - /// - /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. - /// TODO: Save the folder to the server and state. - pub fn edit_without_state( - &self, - old_folder: Folder, - folder: FolderAddEditRequest, - ) -> Result { - self.encrypt(FolderView { - id: old_folder.id, - name: folder.name, - revision_date: old_folder.revision_date, - }) - } } From 40335fa460766e476c67e726df71f3cd7573733c Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:19:23 +0200 Subject: [PATCH 12/28] Remove more folder changes --- crates/bitwarden-vault/src/folder.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 016737132..8168b97aa 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -16,18 +16,16 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug, Clone)] +#[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Folder { - pub id: Option, - pub name: EncString, - pub revision_date: DateTime, + id: Option, + name: EncString, + revision_date: DateTime, } -bitwarden_state::register_repository_item!(Folder, "Folder"); - #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] From bc6f217f6c7ccb1ed860bc4f8a2f249ad9812dc3 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:20:16 +0200 Subject: [PATCH 13/28] Remove folder from wasm --- crates/bitwarden-wasm-internal/src/platform/mod.rs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 9d6c4274c..8b71f5867 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -31,7 +31,6 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); -repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -39,9 +38,4 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } - - pub fn register_folder_repository(&self, store: FolderRepository) { - let store = store.into_channel_impl(); - self.0.platform().state().register_client_managed(store) - } } From 5bb5b4d2de527e8f4dd594ee58f87907710f139e Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:20:58 +0200 Subject: [PATCH 14/28] lint --- crates/bitwarden-wasm-internal/src/platform/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 8b71f5867..1bb5b551b 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::{Cipher, Folder}; +use bitwarden_vault::Cipher; use wasm_bindgen::prelude::wasm_bindgen; mod repository; From caec5c526da1f7017d4eafc0597a81ba6f8cc831 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:41:24 +0200 Subject: [PATCH 15/28] Document and clean up --- crates/bitwarden-core/src/auth/renew.rs | 2 +- crates/bitwarden-core/src/client/client.rs | 10 +++++----- crates/bitwarden-core/src/client/internal.rs | 6 +++--- .../src/platform/token_provider.rs | 1 + 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/crates/bitwarden-core/src/auth/renew.rs b/crates/bitwarden-core/src/auth/renew.rs index c5b5093af..959977f32 100644 --- a/crates/bitwarden-core/src/auth/renew.rs +++ b/crates/bitwarden-core/src/auth/renew.rs @@ -40,7 +40,7 @@ async fn renew_token_client_managed( .get_access_token() .await .ok_or(NotAuthenticatedError)?; - client.set_tokens_internal(token); + client.set_api_tokens_internal(token); Ok(()) } diff --git a/crates/bitwarden-core/src/client/client.rs b/crates/bitwarden-core/src/client/client.rs index d1f5ccf1a..6cd632eb4 100644 --- a/crates/bitwarden-core/src/client/client.rs +++ b/crates/bitwarden-core/src/client/client.rs @@ -25,20 +25,20 @@ pub struct Client { } impl Client { - #[allow(missing_docs)] + /// Create a new Bitwarden client with SDK-managed tokens. pub fn new(settings: Option) -> Self { - Self::new_tokens(settings, Tokens::SdkManaged(SdkManagedTokens::default())) + Self::new_internal(settings, Tokens::SdkManaged(SdkManagedTokens::default())) } - #[allow(missing_docs)] + /// Create a new Bitwarden client with client-managed tokens. pub fn new_with_client_tokens( settings: Option, tokens: Arc, ) -> Self { - Self::new_tokens(settings, Tokens::ClientManaged(tokens)) + Self::new_internal(settings, Tokens::ClientManaged(tokens)) } - fn new_tokens(settings_input: Option, tokens: Tokens) -> Self { + fn new_internal(settings_input: Option, tokens: Tokens) -> Self { let settings = settings_input.unwrap_or_default(); fn new_client_builder() -> reqwest::ClientBuilder { diff --git a/crates/bitwarden-core/src/client/internal.rs b/crates/bitwarden-core/src/client/internal.rs index 08cbc3db3..5c0e085b5 100644 --- a/crates/bitwarden-core/src/client/internal.rs +++ b/crates/bitwarden-core/src/client/internal.rs @@ -138,11 +138,11 @@ impl InternalClient { expires_on: Some(Utc::now().timestamp() + expires_in as i64), refresh_token, }); - self.set_tokens_internal(token); + self.set_api_tokens_internal(token); } - /// Used to set tokens for internal API clients, use `set_tokens` for SdkManagedTokens. - pub(crate) fn set_tokens_internal(&self, token: String) { + /// Sets api tokens for only internal API clients, use `set_tokens` for SdkManagedTokens. + pub(crate) fn set_api_tokens_internal(&self, token: String) { let mut guard = self .__api_configurations .write() diff --git a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs index 2cbc67d87..461bc8b26 100644 --- a/crates/bitwarden-wasm-internal/src/platform/token_provider.rs +++ b/crates/bitwarden-wasm-internal/src/platform/token_provider.rs @@ -18,6 +18,7 @@ extern "C" { pub async fn get_access_token(this: &JsTokenProvider) -> JsValue; } +/// Thread-bound runner for JavaScript token provider pub(crate) struct WasmClientManagedTokens(ThreadBoundRunner); impl WasmClientManagedTokens { From 5c67cd4621f1dfb0affcdc0207a1cb01adbefbd6 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 10:43:07 +0200 Subject: [PATCH 16/28] Remove wasm bindgen futures from vault --- Cargo.lock | 1 - crates/bitwarden-vault/Cargo.toml | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09378f89..82d1c09b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,7 +707,6 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", - "wasm-bindgen-futures", ] [[package]] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index d49b95274..e06ce1460 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,8 +23,7 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify-next", - "dep:wasm-bindgen", - "dep:wasm-bindgen-futures" + "dep:wasm-bindgen" ] # WASM support [dependencies] @@ -49,7 +48,6 @@ tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } -wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } From 9ef95f8a907ec57b95f0d960e5c30d8a0b613c59 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 11:34:05 +0200 Subject: [PATCH 17/28] Restore --- Cargo.lock | 1 + crates/bitwarden-vault/Cargo.toml | 4 +- crates/bitwarden-vault/src/folder.rs | 10 +- crates/bitwarden-vault/src/folder_client.rs | 91 ++++++++++++++++++- .../src/platform/mod.rs | 8 +- 5 files changed, 106 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 82d1c09b0..d09378f89 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -707,6 +707,7 @@ dependencies = [ "uniffi", "uuid", "wasm-bindgen", + "wasm-bindgen-futures", ] [[package]] diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index e06ce1460..d49b95274 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -23,7 +23,8 @@ uniffi = [ wasm = [ "bitwarden-core/wasm", "dep:tsify-next", - "dep:wasm-bindgen" + "dep:wasm-bindgen", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] @@ -48,6 +49,7 @@ tsify-next = { workspace = true, optional = true } uniffi = { workspace = true, optional = true } uuid = { workspace = true } wasm-bindgen = { workspace = true, optional = true } +wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 8168b97aa..016737132 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -16,16 +16,18 @@ use {tsify_next::Tsify, wasm_bindgen::prelude::*}; use crate::VaultParseError; #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct Folder { - id: Option, - name: EncString, - revision_date: DateTime, + pub id: Option, + pub name: EncString, + pub revision_date: DateTime, } +bitwarden_state::register_repository_item!(Folder, "Folder"); + #[allow(missing_docs)] #[derive(Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 90f62bd0b..667cec384 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,18 +1,53 @@ -use bitwarden_core::Client; +use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; +use bitwarden_core::{require, ApiError, Client, MissingFieldError}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::RepositoryError; +use chrono::Utc; +use serde::{Deserialize, Serialize}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use tsify_next::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - Folder, FolderView, + Folder, FolderView, VaultParseError, }; +/// Request to add or edit a folder. +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[cfg_attr(feature = "uniffi", derive(uniffi::Record))] +#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] +pub struct FolderAddEditRequest { + pub name: String, +} + #[allow(missing_docs)] #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, } +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateFolderError { + #[error(transparent)] + Encrypt(#[from] EncryptError), + #[error(transparent)] + Decrypt(#[from] DecryptError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { #[allow(missing_docs)] @@ -35,4 +70,56 @@ impl FoldersClient { let views = key_store.decrypt_list(&folders)?; Ok(views) } + + /// Create a new folder and save it to the server. + pub async fn create( + &self, + request: FolderAddEditRequest, + ) -> Result { + // TODO: We should probably not use a Folder model here, but rather create + // FolderRequestModel directly? + let folder = self.encrypt(FolderView { + id: None, + name: request.name, + revision_date: Utc::now(), + })?; + + let config = self.client.internal.get_api_configurations().await; + let resp = folders_api::folders_post( + &config.api, + Some(FolderRequestModel { + name: folder.name.to_string(), + }), + ) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + self.client + .platform() + .state() + .get_client_managed::() + .map_err(CreateFolderError::Repository)? + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(self.decrypt(folder)?) + } + + /// Edit the folder. + /// + /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. + /// TODO: Save the folder to the server and state. + pub fn edit_without_state( + &self, + old_folder: Folder, + folder: FolderAddEditRequest, + ) -> Result { + self.encrypt(FolderView { + id: old_folder.id, + name: folder.name, + revision_date: old_folder.revision_date, + }) + } } diff --git a/crates/bitwarden-wasm-internal/src/platform/mod.rs b/crates/bitwarden-wasm-internal/src/platform/mod.rs index 1bb5b551b..9d6c4274c 100644 --- a/crates/bitwarden-wasm-internal/src/platform/mod.rs +++ b/crates/bitwarden-wasm-internal/src/platform/mod.rs @@ -1,5 +1,5 @@ use bitwarden_core::Client; -use bitwarden_vault::Cipher; +use bitwarden_vault::{Cipher, Folder}; use wasm_bindgen::prelude::wasm_bindgen; mod repository; @@ -31,6 +31,7 @@ impl StateClient { } repository::create_wasm_repository!(CipherRepository, Cipher, "Repository"); +repository::create_wasm_repository!(FolderRepository, Folder, "Repository"); #[wasm_bindgen] impl StateClient { @@ -38,4 +39,9 @@ impl StateClient { let store = store.into_channel_impl(); self.0.platform().state().register_client_managed(store) } + + pub fn register_folder_repository(&self, store: FolderRepository) { + let store = store.into_channel_impl(); + self.0.platform().state().register_client_managed(store) + } } From 7ed42741fe86bcb2b13cd859c5549dd6398d7325 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 12:35:57 +0200 Subject: [PATCH 18/28] Add a test for folder creation --- Cargo.lock | 3 +- crates/bitwarden-state/src/repository.rs | 59 +++++++ crates/bitwarden-vault/Cargo.toml | 5 +- crates/bitwarden-vault/src/folder.rs | 2 +- crates/bitwarden-vault/src/folder_client.rs | 186 ++++++++++++++++---- 5 files changed, 212 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d09378f89..c33061970 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -609,7 +609,7 @@ name = "bitwarden-state" version = "1.0.0" dependencies = [ "async-trait", - "thiserror 1.0.69", + "thiserror 2.0.12", "tokio", ] @@ -708,6 +708,7 @@ dependencies = [ "uuid", "wasm-bindgen", "wasm-bindgen-futures", + "wiremock", ] [[package]] diff --git a/crates/bitwarden-state/src/repository.rs b/crates/bitwarden-state/src/repository.rs index 4d186455d..15f2b213c 100644 --- a/crates/bitwarden-state/src/repository.rs +++ b/crates/bitwarden-state/src/repository.rs @@ -28,6 +28,65 @@ pub trait Repository: Send + Sync { async fn remove(&self, key: String) -> Result<(), RepositoryError>; } +/// A simple in-memory repository implementation that uses a `HashMap` to store items. +/// +/// Primarily used for testing and development purposes. +pub struct MemoryRepository { + store: std::sync::Mutex>, +} + +impl MemoryRepository { + /// Creates a new `MemoryRepository`. + pub fn new() -> Self { + MemoryRepository { + store: std::sync::Mutex::new(std::collections::HashMap::new()), + } + } +} + +impl Default for MemoryRepository { + fn default() -> Self { + Self::new() + } +} + +#[async_trait::async_trait] +impl Repository for MemoryRepository { + async fn get(&self, key: String) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.get(&key).cloned()) + } + + async fn list(&self) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.values().cloned().collect()) + } + + async fn set(&self, key: String, value: V) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.insert(key, value); + Ok(()) + } + + async fn remove(&self, key: String) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.remove(&key); + Ok(()) + } +} + /// This trait is used to mark types that can be stored in a repository. /// It should not be implemented manually; instead, users should /// use the [crate::register_repository_item] macro to register their item types. diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index d49b95274..23771f0ce 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -18,13 +18,13 @@ keywords.workspace = true uniffi = [ "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", - "dep:uniffi" + "dep:uniffi", ] # Uniffi bindings wasm = [ "bitwarden-core/wasm", "dep:tsify-next", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures" + "dep:wasm-bindgen-futures", ] # WASM support [dependencies] @@ -53,6 +53,7 @@ wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } +wiremock = "0.6.0" [lints] workspace = true diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder.rs index 016737132..070b17e84 100644 --- a/crates/bitwarden-vault/src/folder.rs +++ b/crates/bitwarden-vault/src/folder.rs @@ -29,7 +29,7 @@ pub struct Folder { bitwarden_state::register_repository_item!(Folder, "Folder"); #[allow(missing_docs)] -#[derive(Serialize, Deserialize, Debug)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[serde(rename_all = "camelCase")] #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder_client.rs index 667cec384..1fe416192 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder_client.rs @@ -1,8 +1,15 @@ +use std::sync::Arc; + use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; -use bitwarden_core::{require, ApiError, Client, MissingFieldError}; +use bitwarden_core::{ + key_management::{KeyIds, SymmetricKeyId}, + require, ApiError, Client, MissingFieldError, +}; +use bitwarden_crypto::{ + CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, +}; use bitwarden_error::bitwarden_error; -use bitwarden_state::repository::RepositoryError; -use chrono::Utc; +use bitwarden_state::repository::{Repository, RepositoryError}; use serde::{Deserialize, Serialize}; use thiserror::Error; #[cfg(feature = "wasm")] @@ -35,9 +42,7 @@ pub struct FoldersClient { #[derive(Debug, Error)] pub enum CreateFolderError { #[error(transparent)] - Encrypt(#[from] EncryptError), - #[error(transparent)] - Decrypt(#[from] DecryptError), + Crypto(#[from] CryptoError), #[error(transparent)] Api(#[from] ApiError), #[error(transparent)] @@ -76,50 +81,153 @@ impl FoldersClient { &self, request: FolderAddEditRequest, ) -> Result { - // TODO: We should probably not use a Folder model here, but rather create - // FolderRequestModel directly? - let folder = self.encrypt(FolderView { - id: None, - name: request.name, - revision_date: Utc::now(), - })?; + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + create_folder(key_store, request, &config.api, &repository).await + } + + /// Edit the folder. + pub async fn edit( + &self, + folder_id: &str, + request: FolderAddEditRequest, + ) -> Result { + let repository = self.get_repository()?; + let key_store = self.client.internal.get_key_store(); + + // Check if the folder exists + repository + .get(folder_id.to_owned()) + .await? + .ok_or(MissingFieldError("Folder not found in repository"))?; + + let folder_request = key_store.encrypt(request)?; let config = self.client.internal.get_api_configurations().await; - let resp = folders_api::folders_post( - &config.api, - Some(FolderRequestModel { - name: folder.name.to_string(), - }), - ) - .await - .map_err(ApiError::from)?; + let resp = folders_api::folders_id_put(&config.api, folder_id, Some(folder_request)) + .await + .map_err(ApiError::from)?; let folder: Folder = resp.try_into()?; - self.client - .platform() - .state() - .get_client_managed::() - .map_err(CreateFolderError::Repository)? + repository .set(require!(folder.id).to_string(), folder.clone()) .await?; - Ok(self.decrypt(folder)?) + Ok(key_store.decrypt(&folder)?) } +} - /// Edit the folder. - /// - /// TODO: Replace `old_folder` with `FolderId` and load the old folder from state. - /// TODO: Save the folder to the server and state. - pub fn edit_without_state( +impl FoldersClient { + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self + .client + .platform() + .state() + .get_client_managed::()?) + } +} + +impl CompositeEncryptable for FolderAddEditRequest { + fn encrypt_composite( &self, - old_folder: Folder, - folder: FolderAddEditRequest, - ) -> Result { - self.encrypt(FolderView { - id: old_folder.id, - name: folder.name, - revision_date: old_folder.revision_date, + ctx: &mut KeyStoreContext, + key: SymmetricKeyId, + ) -> Result { + Ok(FolderRequestModel { + name: self.name.encrypt(ctx, key)?.to_string(), }) } } + +impl IdentifyKey for FolderAddEditRequest { + fn key_identifier(&self) -> SymmetricKeyId { + SymmetricKeyId::User + } +} + +pub async fn create_folder + ?Sized>( + key_store: &KeyStore, + request: FolderAddEditRequest, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &Arc, +) -> Result { + let folder_request = key_store.encrypt(request)?; + let resp = folders_api::folders_post(api_config, Some(folder_request)) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + repository + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(key_store.decrypt(&folder)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use bitwarden_api_api::models::FolderResponseModel; + use bitwarden_crypto::SymmetricCryptoKey; + use bitwarden_state::repository::MemoryRepository; + use uuid::uuid; + use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + + #[tokio::test] + async fn test_create_folder_flow_success() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let server = MockServer::start().await; + server + .register( + Mock::given(matchers::path("/folders")) + .respond_with(|req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(FolderResponseModel { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("folder".to_string()), + }) + }) + .expect(1), + ) + .await; + + let request = FolderAddEditRequest { + name: "test".to_string(), + }; + let api_config = &bitwarden_api_api::apis::configuration::Configuration { + base_path: server.uri(), + user_agent: Some("test-agent".to_string()), + client: reqwest::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + }; + let repository = Arc::new(MemoryRepository::::new()); + + let result = create_folder(&store, request, api_config, &repository) + .await + .unwrap(); + + assert_eq!( + result, + FolderView { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: "test".to_string(), + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + ); + } +} From f462d5de1c6dd4c3d2eefc321cd5d0e405f392e4 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 12:47:05 +0200 Subject: [PATCH 19/28] Reorganize --- .../src/folder/folder_client.rs | 97 ++++++++++++++ .../folder_create.rs} | 125 +++--------------- .../{folder.rs => folder/folder_models.rs} | 0 crates/bitwarden-vault/src/folder/mod.rs | 7 + crates/bitwarden-vault/src/lib.rs | 4 +- 5 files changed, 125 insertions(+), 108 deletions(-) create mode 100644 crates/bitwarden-vault/src/folder/folder_client.rs rename crates/bitwarden-vault/src/{folder_client.rs => folder/folder_create.rs} (62%) rename crates/bitwarden-vault/src/{folder.rs => folder/folder_models.rs} (100%) create mode 100644 crates/bitwarden-vault/src/folder/mod.rs diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs new file mode 100644 index 000000000..abfe779a2 --- /dev/null +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -0,0 +1,97 @@ +use std::sync::Arc; + +use bitwarden_api_api::apis::folders_api; +use bitwarden_core::{require, ApiError, Client, MissingFieldError}; + +use bitwarden_state::repository::{Repository, RepositoryError}; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{ + error::{DecryptError, EncryptError}, + folder::create_folder, + CreateFolderError, Folder, FolderAddEditRequest, FolderView, +}; + +#[allow(missing_docs)] +#[cfg_attr(feature = "wasm", wasm_bindgen)] +pub struct FoldersClient { + pub(crate) client: Client, +} + +#[cfg_attr(feature = "wasm", wasm_bindgen)] +impl FoldersClient { + #[allow(missing_docs)] + pub fn encrypt(&self, folder_view: FolderView) -> Result { + let key_store = self.client.internal.get_key_store(); + let folder = key_store.encrypt(folder_view)?; + Ok(folder) + } + + #[allow(missing_docs)] + pub fn decrypt(&self, folder: Folder) -> Result { + let key_store = self.client.internal.get_key_store(); + let folder_view = key_store.decrypt(&folder)?; + Ok(folder_view) + } + + #[allow(missing_docs)] + pub fn decrypt_list(&self, folders: Vec) -> Result, DecryptError> { + let key_store = self.client.internal.get_key_store(); + let views = key_store.decrypt_list(&folders)?; + Ok(views) + } + + /// Create a new folder and save it to the server. + pub async fn create( + &self, + request: FolderAddEditRequest, + ) -> Result { + let key_store = self.client.internal.get_key_store(); + let config = self.client.internal.get_api_configurations().await; + let repository = self.get_repository()?; + + create_folder(key_store, request, &config.api, &repository).await + } + + /// Edit the folder. + pub async fn edit( + &self, + folder_id: &str, + request: FolderAddEditRequest, + ) -> Result { + let repository = self.get_repository()?; + let key_store = self.client.internal.get_key_store(); + + // Check if the folder exists + repository + .get(folder_id.to_owned()) + .await? + .ok_or(MissingFieldError("Folder not found in repository"))?; + + let folder_request = key_store.encrypt(request)?; + + let config = self.client.internal.get_api_configurations().await; + let resp = folders_api::folders_id_put(&config.api, folder_id, Some(folder_request)) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + repository + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(key_store.decrypt(&folder)?) + } +} + +impl FoldersClient { + fn get_repository(&self) -> Result>, RepositoryError> { + Ok(self + .client + .platform() + .state() + .get_client_managed::()?) + } +} diff --git a/crates/bitwarden-vault/src/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_create.rs similarity index 62% rename from crates/bitwarden-vault/src/folder_client.rs rename to crates/bitwarden-vault/src/folder/folder_create.rs index 1fe416192..cee6d61a8 100644 --- a/crates/bitwarden-vault/src/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_create.rs @@ -3,7 +3,7 @@ use std::sync::Arc; use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, - require, ApiError, Client, MissingFieldError, + require, ApiError, MissingFieldError, }; use bitwarden_crypto::{ CompositeEncryptable, CryptoError, IdentifyKey, KeyStore, KeyStoreContext, PrimitiveEncryptable, @@ -17,10 +17,7 @@ use tsify_next::Tsify; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{ - error::{DecryptError, EncryptError}, - Folder, FolderView, VaultParseError, -}; +use crate::{Folder, FolderView, VaultParseError}; /// Request to add or edit a folder. #[derive(Serialize, Deserialize, Debug)] @@ -28,108 +25,10 @@ use crate::{ #[cfg_attr(feature = "uniffi", derive(uniffi::Record))] #[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))] pub struct FolderAddEditRequest { + /// The new name of the folder. pub name: String, } -#[allow(missing_docs)] -#[cfg_attr(feature = "wasm", wasm_bindgen)] -pub struct FoldersClient { - pub(crate) client: Client, -} - -#[allow(missing_docs)] -#[bitwarden_error(flat)] -#[derive(Debug, Error)] -pub enum CreateFolderError { - #[error(transparent)] - Crypto(#[from] CryptoError), - #[error(transparent)] - Api(#[from] ApiError), - #[error(transparent)] - VaultParse(#[from] VaultParseError), - #[error(transparent)] - MissingField(#[from] MissingFieldError), - #[error(transparent)] - RepositoryError(#[from] RepositoryError), -} - -#[cfg_attr(feature = "wasm", wasm_bindgen)] -impl FoldersClient { - #[allow(missing_docs)] - pub fn encrypt(&self, folder_view: FolderView) -> Result { - let key_store = self.client.internal.get_key_store(); - let folder = key_store.encrypt(folder_view)?; - Ok(folder) - } - - #[allow(missing_docs)] - pub fn decrypt(&self, folder: Folder) -> Result { - let key_store = self.client.internal.get_key_store(); - let folder_view = key_store.decrypt(&folder)?; - Ok(folder_view) - } - - #[allow(missing_docs)] - pub fn decrypt_list(&self, folders: Vec) -> Result, DecryptError> { - let key_store = self.client.internal.get_key_store(); - let views = key_store.decrypt_list(&folders)?; - Ok(views) - } - - /// Create a new folder and save it to the server. - pub async fn create( - &self, - request: FolderAddEditRequest, - ) -> Result { - let key_store = self.client.internal.get_key_store(); - let config = self.client.internal.get_api_configurations().await; - let repository = self.get_repository()?; - - create_folder(key_store, request, &config.api, &repository).await - } - - /// Edit the folder. - pub async fn edit( - &self, - folder_id: &str, - request: FolderAddEditRequest, - ) -> Result { - let repository = self.get_repository()?; - let key_store = self.client.internal.get_key_store(); - - // Check if the folder exists - repository - .get(folder_id.to_owned()) - .await? - .ok_or(MissingFieldError("Folder not found in repository"))?; - - let folder_request = key_store.encrypt(request)?; - - let config = self.client.internal.get_api_configurations().await; - let resp = folders_api::folders_id_put(&config.api, folder_id, Some(folder_request)) - .await - .map_err(ApiError::from)?; - - let folder: Folder = resp.try_into()?; - - repository - .set(require!(folder.id).to_string(), folder.clone()) - .await?; - - Ok(key_store.decrypt(&folder)?) - } -} - -impl FoldersClient { - fn get_repository(&self) -> Result>, RepositoryError> { - Ok(self - .client - .platform() - .state() - .get_client_managed::()?) - } -} - impl CompositeEncryptable for FolderAddEditRequest { fn encrypt_composite( &self, @@ -148,7 +47,23 @@ impl IdentifyKey for FolderAddEditRequest { } } -pub async fn create_folder + ?Sized>( +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum CreateFolderError { + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +pub(super) async fn create_folder + ?Sized>( key_store: &KeyStore, request: FolderAddEditRequest, api_config: &bitwarden_api_api::apis::configuration::Configuration, diff --git a/crates/bitwarden-vault/src/folder.rs b/crates/bitwarden-vault/src/folder/folder_models.rs similarity index 100% rename from crates/bitwarden-vault/src/folder.rs rename to crates/bitwarden-vault/src/folder/folder_models.rs diff --git a/crates/bitwarden-vault/src/folder/mod.rs b/crates/bitwarden-vault/src/folder/mod.rs new file mode 100644 index 000000000..41e4ac045 --- /dev/null +++ b/crates/bitwarden-vault/src/folder/mod.rs @@ -0,0 +1,7 @@ +mod folder_client; +mod folder_create; +mod folder_models; + +pub use folder_client::*; +pub use folder_create::*; +pub use folder_models::*; diff --git a/crates/bitwarden-vault/src/lib.rs b/crates/bitwarden-vault/src/lib.rs index e1d893cc2..6c7a691f0 100644 --- a/crates/bitwarden-vault/src/lib.rs +++ b/crates/bitwarden-vault/src/lib.rs @@ -12,9 +12,7 @@ pub use collection::{Collection, CollectionView}; mod collection_client; pub use collection_client::CollectionsClient; mod folder; -pub use folder::{Folder, FolderView}; -mod folder_client; -pub use folder_client::FoldersClient; +pub use folder::*; mod password_history; pub use password_history::{PasswordHistory, PasswordHistoryView}; mod password_history_client; From 25e12333a2b547faffd3fcdbc5c1f6480ae08518 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 18:49:57 +0200 Subject: [PATCH 20/28] Edit --- .../src/folder/folder_client.rs | 33 +--- .../src/folder/folder_create.rs | 6 +- .../bitwarden-vault/src/folder/folder_edit.rs | 146 ++++++++++++++++++ crates/bitwarden-vault/src/folder/mod.rs | 2 + 4 files changed, 158 insertions(+), 29 deletions(-) create mode 100644 crates/bitwarden-vault/src/folder/folder_edit.rs diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs index abfe779a2..831adaa66 100644 --- a/crates/bitwarden-vault/src/folder/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -1,7 +1,6 @@ use std::sync::Arc; -use bitwarden_api_api::apis::folders_api; -use bitwarden_core::{require, ApiError, Client, MissingFieldError}; +use bitwarden_core::Client; use bitwarden_state::repository::{Repository, RepositoryError}; #[cfg(feature = "wasm")] @@ -9,8 +8,8 @@ use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - folder::create_folder, - CreateFolderError, Folder, FolderAddEditRequest, FolderView, + folder::{create_folder, edit_folder}, + CreateFolderError, EditFolderError, Folder, FolderAddEditRequest, FolderView, }; #[allow(missing_docs)] @@ -51,7 +50,7 @@ impl FoldersClient { let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - create_folder(key_store, request, &config.api, &repository).await + create_folder(key_store, &config.api, &repository, request).await } /// Edit the folder. @@ -59,30 +58,12 @@ impl FoldersClient { &self, folder_id: &str, request: FolderAddEditRequest, - ) -> Result { - let repository = self.get_repository()?; + ) -> Result { let key_store = self.client.internal.get_key_store(); - - // Check if the folder exists - repository - .get(folder_id.to_owned()) - .await? - .ok_or(MissingFieldError("Folder not found in repository"))?; - - let folder_request = key_store.encrypt(request)?; - let config = self.client.internal.get_api_configurations().await; - let resp = folders_api::folders_id_put(&config.api, folder_id, Some(folder_request)) - .await - .map_err(ApiError::from)?; - - let folder: Folder = resp.try_into()?; - - repository - .set(require!(folder.id).to_string(), folder.clone()) - .await?; + let repository = self.get_repository()?; - Ok(key_store.decrypt(&folder)?) + edit_folder(key_store, &config.api, &repository, folder_id, request).await } } diff --git a/crates/bitwarden-vault/src/folder/folder_create.rs b/crates/bitwarden-vault/src/folder/folder_create.rs index cee6d61a8..3d24946af 100644 --- a/crates/bitwarden-vault/src/folder/folder_create.rs +++ b/crates/bitwarden-vault/src/folder/folder_create.rs @@ -65,9 +65,9 @@ pub enum CreateFolderError { pub(super) async fn create_folder + ?Sized>( key_store: &KeyStore, - request: FolderAddEditRequest, api_config: &bitwarden_api_api::apis::configuration::Configuration, repository: &Arc, + request: FolderAddEditRequest, ) -> Result { let folder_request = key_store.encrypt(request)?; let resp = folders_api::folders_post(api_config, Some(folder_request)) @@ -93,7 +93,7 @@ mod tests { use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; #[tokio::test] - async fn test_create_folder_flow_success() { + async fn test_create_folder() { let store: KeyStore = KeyStore::default(); #[allow(deprecated)] let _ = store.context_mut().set_symmetric_key( @@ -132,7 +132,7 @@ mod tests { }; let repository = Arc::new(MemoryRepository::::new()); - let result = create_folder(&store, request, api_config, &repository) + let result = create_folder(&store, api_config, &repository, request) .await .unwrap(); diff --git a/crates/bitwarden-vault/src/folder/folder_edit.rs b/crates/bitwarden-vault/src/folder/folder_edit.rs new file mode 100644 index 000000000..8088c800b --- /dev/null +++ b/crates/bitwarden-vault/src/folder/folder_edit.rs @@ -0,0 +1,146 @@ +use std::sync::Arc; + +use bitwarden_api_api::apis::folders_api; +use bitwarden_core::{key_management::KeyIds, require, ApiError, MissingFieldError}; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; +#[cfg(feature = "wasm")] +use wasm_bindgen::prelude::*; + +use crate::{Folder, FolderAddEditRequest, FolderView, VaultParseError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum EditFolderError { + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + Api(#[from] ApiError), + #[error(transparent)] + VaultParse(#[from] VaultParseError), + #[error(transparent)] + MissingField(#[from] MissingFieldError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), + #[error(transparent)] + Uuid(#[from] uuid::Error), +} + +pub(super) async fn edit_folder + ?Sized>( + key_store: &KeyStore, + api_config: &bitwarden_api_api::apis::configuration::Configuration, + repository: &Arc, + folder_id: &str, + request: FolderAddEditRequest, +) -> Result { + // Check if the folder exists + repository + .get(folder_id.to_owned()) + .await? + .ok_or(MissingFieldError("Folder not found in repository"))?; + + let folder_request = key_store.encrypt(request)?; + + let resp = folders_api::folders_id_put(api_config, folder_id, Some(folder_request)) + .await + .map_err(ApiError::from)?; + + let folder: Folder = resp.try_into()?; + + repository + .set(require!(folder.id).to_string(), folder.clone()) + .await?; + + Ok(key_store.decrypt(&folder)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use bitwarden_api_api::models::{FolderRequestModel, FolderResponseModel}; + use bitwarden_core::key_management::SymmetricKeyId; + use bitwarden_crypto::{PrimitiveEncryptable, SymmetricCryptoKey}; + use bitwarden_state::repository::MemoryRepository; + use uuid::uuid; + use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + + //#[tokio::test] + async fn test_edit_folder() { + let store: KeyStore = KeyStore::default(); + let mut ctx = store.context_mut(); + #[allow(deprecated)] + let _ = ctx.set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let folder_id = "25afb11c-9c95-4db5-8bac-c21cb204a3f1".to_owned(); + + let server = MockServer::start().await; + server + .register( + Mock::given(matchers::path( + "/folders/25afb11c-9c95-4db5-8bac-c21cb204a3f1", + )) + .respond_with(|req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(FolderResponseModel { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("folder".to_string()), + }) + }) + .expect(1), + ) + .await; + + let request = FolderAddEditRequest { + name: "test".to_string(), + }; + let api_config = &bitwarden_api_api::apis::configuration::Configuration { + base_path: server.uri(), + user_agent: Some("test-agent".to_string()), + client: reqwest::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + }; + let repository = Arc::new(MemoryRepository::::new()); + + repository + .set( + folder_id, + Folder { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: "old_name".encrypt(&mut ctx, SymmetricKeyId::User).unwrap(), + revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), + }, + ) + .await + .unwrap(); + + let result = edit_folder( + &store, + api_config, + &repository, + "25afb11c-9c95-4db5-8bac-c21cb204a3f1", + request, + ) + .await + .unwrap(); + + assert_eq!( + result, + FolderView { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: "test".to_string(), + revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), + } + ); + } +} diff --git a/crates/bitwarden-vault/src/folder/mod.rs b/crates/bitwarden-vault/src/folder/mod.rs index 41e4ac045..aad88ed39 100644 --- a/crates/bitwarden-vault/src/folder/mod.rs +++ b/crates/bitwarden-vault/src/folder/mod.rs @@ -1,7 +1,9 @@ mod folder_client; mod folder_create; +mod folder_edit; mod folder_models; pub use folder_client::*; pub use folder_create::*; +pub use folder_edit::*; pub use folder_models::*; From b30d0b6e7639ff1fc895737c72f3f25490fd4230 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 18:52:52 +0200 Subject: [PATCH 21/28] Fix test hangs --- crates/bitwarden-vault/src/folder/folder_edit.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/crates/bitwarden-vault/src/folder/folder_edit.rs b/crates/bitwarden-vault/src/folder/folder_edit.rs index 8088c800b..ff559fcc2 100644 --- a/crates/bitwarden-vault/src/folder/folder_edit.rs +++ b/crates/bitwarden-vault/src/folder/folder_edit.rs @@ -67,12 +67,11 @@ mod tests { use uuid::uuid; use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; - //#[tokio::test] + #[tokio::test] async fn test_edit_folder() { let store: KeyStore = KeyStore::default(); - let mut ctx = store.context_mut(); #[allow(deprecated)] - let _ = ctx.set_symmetric_key( + let _ = store.context_mut().set_symmetric_key( SymmetricKeyId::User, SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); @@ -117,7 +116,9 @@ mod tests { folder_id, Folder { id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), - name: "old_name".encrypt(&mut ctx, SymmetricKeyId::User).unwrap(), + name: "old_name" + .encrypt(&mut store.context_mut(), SymmetricKeyId::User) + .unwrap(), revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), }, ) From 78d1cfba979a9fb58e6b8780fe0122f02d928d83 Mon Sep 17 00:00:00 2001 From: Hinton Date: Tue, 1 Jul 2025 18:59:00 +0200 Subject: [PATCH 22/28] fmt --- .../src/folder/folder_client.rs | 3 +- .../src/folder/folder_create.rs | 3 +- .../bitwarden-vault/src/folder/folder_edit.rs | 33 +++++++++---------- 3 files changed, 19 insertions(+), 20 deletions(-) diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs index 831adaa66..e2f552465 100644 --- a/crates/bitwarden-vault/src/folder/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -1,7 +1,6 @@ use std::sync::Arc; use bitwarden_core::Client; - use bitwarden_state::repository::{Repository, RepositoryError}; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; @@ -53,7 +52,7 @@ impl FoldersClient { create_folder(key_store, &config.api, &repository, request).await } - /// Edit the folder. + /// Edit the folder and save it to the server. pub async fn edit( &self, folder_id: &str, diff --git a/crates/bitwarden-vault/src/folder/folder_create.rs b/crates/bitwarden-vault/src/folder/folder_create.rs index 3d24946af..488112edb 100644 --- a/crates/bitwarden-vault/src/folder/folder_create.rs +++ b/crates/bitwarden-vault/src/folder/folder_create.rs @@ -85,13 +85,14 @@ pub(super) async fn create_folder + ?Sized>( #[cfg(test)] mod tests { - use super::*; use bitwarden_api_api::models::FolderResponseModel; use bitwarden_crypto::SymmetricCryptoKey; use bitwarden_state::repository::MemoryRepository; use uuid::uuid; use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + use super::*; + #[tokio::test] async fn test_create_folder() { let store: KeyStore = KeyStore::default(); diff --git a/crates/bitwarden-vault/src/folder/folder_edit.rs b/crates/bitwarden-vault/src/folder/folder_edit.rs index ff559fcc2..cbf049779 100644 --- a/crates/bitwarden-vault/src/folder/folder_edit.rs +++ b/crates/bitwarden-vault/src/folder/folder_edit.rs @@ -59,7 +59,6 @@ pub(super) async fn edit_folder + ?Sized>( #[cfg(test)] mod tests { - use super::*; use bitwarden_api_api::models::{FolderRequestModel, FolderResponseModel}; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{PrimitiveEncryptable, SymmetricCryptoKey}; @@ -67,6 +66,8 @@ mod tests { use uuid::uuid; use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + use super::*; + #[tokio::test] async fn test_edit_folder() { let store: KeyStore = KeyStore::default(); @@ -76,24 +77,22 @@ mod tests { SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); - let folder_id = "25afb11c-9c95-4db5-8bac-c21cb204a3f1".to_owned(); + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); let server = MockServer::start().await; server .register( - Mock::given(matchers::path( - "/folders/25afb11c-9c95-4db5-8bac-c21cb204a3f1", - )) - .respond_with(|req: &Request| { - let body: FolderRequestModel = req.body_json().unwrap(); - ResponseTemplate::new(201).set_body_json(FolderResponseModel { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), - name: Some(body.name), - revision_date: Some("2025-01-01T00:00:00Z".to_string()), - object: Some("folder".to_string()), + Mock::given(matchers::path(format!("/folders/{}", folder_id))) + .respond_with(move |req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(FolderResponseModel { + id: Some(folder_id), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("folder".to_string()), + }) }) - }) - .expect(1), + .expect(1), ) .await; @@ -113,9 +112,9 @@ mod tests { repository .set( - folder_id, + folder_id.to_string(), Folder { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + id: Some(folder_id), name: "old_name" .encrypt(&mut store.context_mut(), SymmetricKeyId::User) .unwrap(), @@ -129,7 +128,7 @@ mod tests { &store, api_config, &repository, - "25afb11c-9c95-4db5-8bac-c21cb204a3f1", + &folder_id.to_string(), request, ) .await From 9528e58e67a307125ca412279eab8e62f80ba651 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 3 Jul 2025 09:56:23 +0200 Subject: [PATCH 23/28] Create bitwarden-test and refactor code --- .vscode/settings.json | 1 + Cargo.lock | 12 ++++ Cargo.toml | 8 ++- bitwarden_license/bitwarden-sm/Cargo.toml | 2 +- crates/bitwarden-generators/Cargo.toml | 2 +- crates/bitwarden-state/src/repository.rs | 59 ---------------- crates/bitwarden-test/Cargo.toml | 25 +++++++ crates/bitwarden-test/README.md | 7 ++ crates/bitwarden-test/src/api.rs | 24 +++++++ crates/bitwarden-test/src/lib.rs | 7 ++ crates/bitwarden-test/src/repository.rs | 54 +++++++++++++++ crates/bitwarden-vault/Cargo.toml | 7 +- .../src/folder/folder_client.rs | 11 ++- .../src/folder/folder_create.rs | 67 ++++++++----------- .../bitwarden-vault/src/folder/folder_edit.rs | 67 ++++++++----------- 15 files changed, 205 insertions(+), 148 deletions(-) create mode 100644 crates/bitwarden-test/Cargo.toml create mode 100644 crates/bitwarden-test/README.md create mode 100644 crates/bitwarden-test/src/api.rs create mode 100644 crates/bitwarden-test/src/lib.rs create mode 100644 crates/bitwarden-test/src/repository.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 0df252c66..49d117177 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,6 +27,7 @@ "spki", "totp", "uniffi", + "wiremock", "wordlist", "XCHACHA", "Zeroize", diff --git a/Cargo.lock b/Cargo.lock index c33061970..26fb9550c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -613,6 +613,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "bitwarden-test" +version = "1.0.0" +dependencies = [ + "async-trait", + "bitwarden-api-api", + "bitwarden-state", + "reqwest", + "wiremock", +] + [[package]] name = "bitwarden-threading" version = "1.0.0" @@ -691,6 +702,7 @@ dependencies = [ "bitwarden-crypto", "bitwarden-error", "bitwarden-state", + "bitwarden-test", "chrono", "data-encoding", "hmac", diff --git a/Cargo.toml b/Cargo.toml index 0527b30d3..3f77e81ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,10 +31,11 @@ bitwarden-fido = { path = "crates/bitwarden-fido", version = "=1.0.0" } bitwarden-generators = { path = "crates/bitwarden-generators", version = "=1.0.0" } bitwarden-ipc = { path = "crates/bitwarden-ipc", version = "=1.0.0" } bitwarden-send = { path = "crates/bitwarden-send", version = "=1.0.0" } -bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" } -bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" } bitwarden-sm = { path = "bitwarden_license/bitwarden-sm", version = "=1.0.0" } bitwarden-ssh = { path = "crates/bitwarden-ssh", version = "=1.0.0" } +bitwarden-state = { path = "crates/bitwarden-state", version = "=1.0.0" } +bitwarden-test = { path = "crates/bitwarden-test", version = "=1.0.0" } +bitwarden-threading = { path = "crates/bitwarden-threading", version = "=1.0.0" } bitwarden-uuid = { path = "crates/bitwarden-uuid", version = "=1.0.0" } bitwarden-uuid-macro = { path = "crates/bitwarden-uuid-macro", version = "=1.0.0" } bitwarden-vault = { path = "crates/bitwarden-vault", version = "=1.0.0" } @@ -60,6 +61,7 @@ serde = { version = ">=1.0, <2.0", features = ["derive"] } serde_json = ">=1.0.96, <2.0" serde_qs = ">=0.12.0, <0.16" serde_repr = ">=0.1.12, <0.2" +serde-wasm-bindgen = ">=0.6.0, <0.7" syn = ">=2.0.87, <3" thiserror = ">=1.0.40, <3" tokio = { version = "1.36.0", features = ["macros"] } @@ -72,7 +74,7 @@ validator = { version = ">=0.18.1, <0.21", features = ["derive"] } wasm-bindgen = { version = ">=0.2.91, <0.3", features = ["serde-serialize"] } wasm-bindgen-futures = "0.4.41" wasm-bindgen-test = "0.3.45" -serde-wasm-bindgen = ">=0.6.0, <0.7" +wiremock = ">=0.6.0, <0.7" # There is an incompatibility when using pkcs5 and chacha20 on wasm builds. This can be removed once a new # rustcrypto-formats crate version is released since the fix has been upstreamed. diff --git a/bitwarden_license/bitwarden-sm/Cargo.toml b/bitwarden_license/bitwarden-sm/Cargo.toml index ce2521938..0eed312d1 100644 --- a/bitwarden_license/bitwarden-sm/Cargo.toml +++ b/bitwarden_license/bitwarden-sm/Cargo.toml @@ -28,7 +28,7 @@ validator = { workspace = true } [dev-dependencies] tokio = { workspace = true, features = ["rt"] } -wiremock = "0.6.0" +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-generators/Cargo.toml b/crates/bitwarden-generators/Cargo.toml index 3b4097ee3..d8b17d173 100644 --- a/crates/bitwarden-generators/Cargo.toml +++ b/crates/bitwarden-generators/Cargo.toml @@ -39,7 +39,7 @@ wasm-bindgen = { workspace = true, optional = true } [dev-dependencies] rand_chacha = "0.3.1" tokio = { workspace = true, features = ["rt"] } -wiremock = "0.6.0" +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-state/src/repository.rs b/crates/bitwarden-state/src/repository.rs index 15f2b213c..4d186455d 100644 --- a/crates/bitwarden-state/src/repository.rs +++ b/crates/bitwarden-state/src/repository.rs @@ -28,65 +28,6 @@ pub trait Repository: Send + Sync { async fn remove(&self, key: String) -> Result<(), RepositoryError>; } -/// A simple in-memory repository implementation that uses a `HashMap` to store items. -/// -/// Primarily used for testing and development purposes. -pub struct MemoryRepository { - store: std::sync::Mutex>, -} - -impl MemoryRepository { - /// Creates a new `MemoryRepository`. - pub fn new() -> Self { - MemoryRepository { - store: std::sync::Mutex::new(std::collections::HashMap::new()), - } - } -} - -impl Default for MemoryRepository { - fn default() -> Self { - Self::new() - } -} - -#[async_trait::async_trait] -impl Repository for MemoryRepository { - async fn get(&self, key: String) -> Result, RepositoryError> { - let store = self - .store - .lock() - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - Ok(store.get(&key).cloned()) - } - - async fn list(&self) -> Result, RepositoryError> { - let store = self - .store - .lock() - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - Ok(store.values().cloned().collect()) - } - - async fn set(&self, key: String, value: V) -> Result<(), RepositoryError> { - let mut store = self - .store - .lock() - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - store.insert(key, value); - Ok(()) - } - - async fn remove(&self, key: String) -> Result<(), RepositoryError> { - let mut store = self - .store - .lock() - .map_err(|e| RepositoryError::Internal(e.to_string()))?; - store.remove(&key); - Ok(()) - } -} - /// This trait is used to mark types that can be stored in a repository. /// It should not be implemented manually; instead, users should /// use the [crate::register_repository_item] macro to register their item types. diff --git a/crates/bitwarden-test/Cargo.toml b/crates/bitwarden-test/Cargo.toml new file mode 100644 index 000000000..0b7f8f481 --- /dev/null +++ b/crates/bitwarden-test/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "bitwarden-test" +description = """ +Internal crate for the bitwarden crate. Do not use. +""" + +version.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true +readme.workspace = true +homepage.workspace = true +repository.workspace = true +license-file.workspace = true +keywords.workspace = true + +[dependencies] +async-trait = { workspace = true } +bitwarden-api-api.workspace = true +bitwarden-state = { workspace = true } +reqwest = { workspace = true } +wiremock = { workspace = true } + +[lints] +workspace = true diff --git a/crates/bitwarden-test/README.md b/crates/bitwarden-test/README.md new file mode 100644 index 000000000..00f329a85 --- /dev/null +++ b/crates/bitwarden-test/README.md @@ -0,0 +1,7 @@ +# Bitwarden Test + +
+This crate should only be used in tests and should not be included in production builds. +
+ +Contains test utilities for Bitwarden. diff --git a/crates/bitwarden-test/src/api.rs b/crates/bitwarden-test/src/api.rs new file mode 100644 index 000000000..265dee15b --- /dev/null +++ b/crates/bitwarden-test/src/api.rs @@ -0,0 +1,24 @@ +use bitwarden_api_api::apis::configuration::Configuration; + +/// Helper for testing the Bitwarden API using wiremock. +/// +/// Warning: when using `Mock::expected` ensure `server` is not dropped before the test completes, +pub async fn start_api_mock(mocks: Vec) -> (wiremock::MockServer, Configuration) { + let server = wiremock::MockServer::start().await; + + for mock in mocks { + server.register(mock).await; + } + + let config = Configuration { + base_path: server.uri(), + user_agent: Some("test-agent".to_string()), + client: reqwest::Client::new(), + basic_auth: None, + oauth_access_token: None, + bearer_access_token: None, + api_key: None, + }; + + (server, config) +} diff --git a/crates/bitwarden-test/src/lib.rs b/crates/bitwarden-test/src/lib.rs new file mode 100644 index 000000000..fcc1930a0 --- /dev/null +++ b/crates/bitwarden-test/src/lib.rs @@ -0,0 +1,7 @@ +#![doc = include_str!("../README.md")] + +mod api; +pub use api::*; + +mod repository; +pub use repository::*; diff --git a/crates/bitwarden-test/src/repository.rs b/crates/bitwarden-test/src/repository.rs new file mode 100644 index 000000000..31d4cb3ce --- /dev/null +++ b/crates/bitwarden-test/src/repository.rs @@ -0,0 +1,54 @@ +use bitwarden_state::repository::{Repository, RepositoryError, RepositoryItem}; + +/// A simple in-memory repository implementation. The data is only stored in memory and will not +/// persist beyond the lifetime of the repository instance. +/// +/// Primary use case is for unit and integration tests. +pub struct MemoryRepository { + store: std::sync::Mutex>, +} + +impl Default for MemoryRepository { + fn default() -> Self { + Self { + store: std::sync::Mutex::new(std::collections::HashMap::new()), + } + } +} + +#[async_trait::async_trait] +impl Repository for MemoryRepository { + async fn get(&self, key: String) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.get(&key).cloned()) + } + + async fn list(&self) -> Result, RepositoryError> { + let store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + Ok(store.values().cloned().collect()) + } + + async fn set(&self, key: String, value: V) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.insert(key, value); + Ok(()) + } + + async fn remove(&self, key: String) -> Result<(), RepositoryError> { + let mut store = self + .store + .lock() + .map_err(|e| RepositoryError::Internal(e.to_string()))?; + store.remove(&key); + Ok(()) + } +} diff --git a/crates/bitwarden-vault/Cargo.toml b/crates/bitwarden-vault/Cargo.toml index 23771f0ce..8ad48ea01 100644 --- a/crates/bitwarden-vault/Cargo.toml +++ b/crates/bitwarden-vault/Cargo.toml @@ -18,13 +18,13 @@ keywords.workspace = true uniffi = [ "bitwarden-core/uniffi", "bitwarden-crypto/uniffi", - "dep:uniffi", + "dep:uniffi" ] # Uniffi bindings wasm = [ "bitwarden-core/wasm", "dep:tsify-next", "dep:wasm-bindgen", - "dep:wasm-bindgen-futures", + "dep:wasm-bindgen-futures" ] # WASM support [dependencies] @@ -52,8 +52,9 @@ wasm-bindgen = { workspace = true, optional = true } wasm-bindgen-futures = { workspace = true, optional = true } [dev-dependencies] +bitwarden-test = { workspace = true } tokio = { workspace = true, features = ["rt"] } -wiremock = "0.6.0" +wiremock = { workspace = true } [lints] workspace = true diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs index e2f552465..5fd6adef9 100644 --- a/crates/bitwarden-vault/src/folder/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -49,7 +49,7 @@ impl FoldersClient { let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - create_folder(key_store, &config.api, &repository, request).await + create_folder(key_store, &config.api, repository.as_ref(), request).await } /// Edit the folder and save it to the server. @@ -62,7 +62,14 @@ impl FoldersClient { let config = self.client.internal.get_api_configurations().await; let repository = self.get_repository()?; - edit_folder(key_store, &config.api, &repository, folder_id, request).await + edit_folder( + key_store, + &config.api, + repository.as_ref(), + folder_id, + request, + ) + .await } } diff --git a/crates/bitwarden-vault/src/folder/folder_create.rs b/crates/bitwarden-vault/src/folder/folder_create.rs index 488112edb..5b18261e3 100644 --- a/crates/bitwarden-vault/src/folder/folder_create.rs +++ b/crates/bitwarden-vault/src/folder/folder_create.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use bitwarden_api_api::{apis::folders_api, models::FolderRequestModel}; use bitwarden_core::{ key_management::{KeyIds, SymmetricKeyId}, @@ -66,7 +64,7 @@ pub enum CreateFolderError { pub(super) async fn create_folder + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, - repository: &Arc, + repository: &R, request: FolderAddEditRequest, ) -> Result { let folder_request = key_store.encrypt(request)?; @@ -87,9 +85,9 @@ pub(super) async fn create_folder + ?Sized>( mod tests { use bitwarden_api_api::models::FolderResponseModel; use bitwarden_crypto::SymmetricCryptoKey; - use bitwarden_state::repository::MemoryRepository; + use bitwarden_test::{start_api_mock, MemoryRepository}; use uuid::uuid; - use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; use super::*; @@ -102,40 +100,31 @@ mod tests { SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); - let server = MockServer::start().await; - server - .register( - Mock::given(matchers::path("/folders")) - .respond_with(|req: &Request| { - let body: FolderRequestModel = req.body_json().unwrap(); - ResponseTemplate::new(201).set_body_json(FolderResponseModel { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), - name: Some(body.name), - revision_date: Some("2025-01-01T00:00:00Z".to_string()), - object: Some("folder".to_string()), - }) - }) - .expect(1), - ) - .await; - - let request = FolderAddEditRequest { - name: "test".to_string(), - }; - let api_config = &bitwarden_api_api::apis::configuration::Configuration { - base_path: server.uri(), - user_agent: Some("test-agent".to_string()), - client: reqwest::Client::new(), - basic_auth: None, - oauth_access_token: None, - bearer_access_token: None, - api_key: None, - }; - let repository = Arc::new(MemoryRepository::::new()); - - let result = create_folder(&store, api_config, &repository, request) - .await - .unwrap(); + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders")) + .respond_with(|req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(201).set_body_json(FolderResponseModel { + id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + object: Some("folder".to_string()), + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); + + let result = create_folder( + &store, + &api_config, + &repository, + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await + .unwrap(); assert_eq!( result, diff --git a/crates/bitwarden-vault/src/folder/folder_edit.rs b/crates/bitwarden-vault/src/folder/folder_edit.rs index cbf049779..ec40a1d36 100644 --- a/crates/bitwarden-vault/src/folder/folder_edit.rs +++ b/crates/bitwarden-vault/src/folder/folder_edit.rs @@ -1,5 +1,3 @@ -use std::sync::Arc; - use bitwarden_api_api::apis::folders_api; use bitwarden_core::{key_management::KeyIds, require, ApiError, MissingFieldError}; use bitwarden_crypto::{CryptoError, KeyStore}; @@ -32,11 +30,11 @@ pub enum EditFolderError { pub(super) async fn edit_folder + ?Sized>( key_store: &KeyStore, api_config: &bitwarden_api_api::apis::configuration::Configuration, - repository: &Arc, + repository: &R, folder_id: &str, request: FolderAddEditRequest, ) -> Result { - // Check if the folder exists + // Verify the folder we're updating exists repository .get(folder_id.to_owned()) .await? @@ -62,9 +60,9 @@ mod tests { use bitwarden_api_api::models::{FolderRequestModel, FolderResponseModel}; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{PrimitiveEncryptable, SymmetricCryptoKey}; - use bitwarden_state::repository::MemoryRepository; + use bitwarden_test::{start_api_mock, MemoryRepository}; use uuid::uuid; - use wiremock::{matchers, Mock, MockServer, Request, ResponseTemplate}; + use wiremock::{matchers, Mock, Request, ResponseTemplate}; use super::*; @@ -79,36 +77,23 @@ mod tests { let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); - let server = MockServer::start().await; - server - .register( - Mock::given(matchers::path(format!("/folders/{}", folder_id))) - .respond_with(move |req: &Request| { - let body: FolderRequestModel = req.body_json().unwrap(); - ResponseTemplate::new(201).set_body_json(FolderResponseModel { - id: Some(folder_id), - name: Some(body.name), - revision_date: Some("2025-01-01T00:00:00Z".to_string()), - object: Some("folder".to_string()), - }) - }) - .expect(1), - ) - .await; - - let request = FolderAddEditRequest { - name: "test".to_string(), - }; - let api_config = &bitwarden_api_api::apis::configuration::Configuration { - base_path: server.uri(), - user_agent: Some("test-agent".to_string()), - client: reqwest::Client::new(), - basic_auth: None, - oauth_access_token: None, - bearer_access_token: None, - api_key: None, - }; - let repository = Arc::new(MemoryRepository::::new()); + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/folders/{}", + folder_id + ))) + .respond_with(move |req: &Request| { + let body: FolderRequestModel = req.body_json().unwrap(); + ResponseTemplate::new(200).set_body_json(FolderResponseModel { + object: Some("folder".to_string()), + id: Some(folder_id), + name: Some(body.name), + revision_date: Some("2025-01-01T00:00:00Z".to_string()), + }) + }) + .expect(1)]) + .await; + + let repository = MemoryRepository::::default(); repository .set( @@ -116,7 +101,7 @@ mod tests { Folder { id: Some(folder_id), name: "old_name" - .encrypt(&mut store.context_mut(), SymmetricKeyId::User) + .encrypt(&mut store.context(), SymmetricKeyId::User) .unwrap(), revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), }, @@ -126,10 +111,12 @@ mod tests { let result = edit_folder( &store, - api_config, + &api_config, &repository, &folder_id.to_string(), - request, + FolderAddEditRequest { + name: "test".to_string(), + }, ) .await .unwrap(); @@ -137,7 +124,7 @@ mod tests { assert_eq!( result, FolderView { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + id: Some(folder_id), name: "test".to_string(), revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), } From 46979ef05317c139a5c4d584bcef48ef438965c1 Mon Sep 17 00:00:00 2001 From: Hinton Date: Thu, 3 Jul 2025 14:17:09 +0200 Subject: [PATCH 24/28] Expand tests --- .../folder/{folder_create.rs => create.rs} | 52 +++++++- .../src/folder/{folder_edit.rs => edit.rs} | 111 +++++++++++++++--- crates/bitwarden-vault/src/folder/mod.rs | 8 +- 3 files changed, 148 insertions(+), 23 deletions(-) rename crates/bitwarden-vault/src/folder/{folder_create.rs => create.rs} (73%) rename crates/bitwarden-vault/src/folder/{folder_edit.rs => edit.rs} (61%) diff --git a/crates/bitwarden-vault/src/folder/folder_create.rs b/crates/bitwarden-vault/src/folder/create.rs similarity index 73% rename from crates/bitwarden-vault/src/folder/folder_create.rs rename to crates/bitwarden-vault/src/folder/create.rs index 5b18261e3..cc583652c 100644 --- a/crates/bitwarden-vault/src/folder/folder_create.rs +++ b/crates/bitwarden-vault/src/folder/create.rs @@ -100,11 +100,13 @@ mod tests { SymmetricCryptoKey::make_aes256_cbc_hmac_key(), ); + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path("/folders")) - .respond_with(|req: &Request| { + .respond_with(move |req: &Request| { let body: FolderRequestModel = req.body_json().unwrap(); ResponseTemplate::new(201).set_body_json(FolderResponseModel { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + id: Some(folder_id), name: Some(body.name), revision_date: Some("2025-01-01T00:00:00Z".to_string()), object: Some("folder".to_string()), @@ -129,10 +131,54 @@ mod tests { assert_eq!( result, FolderView { - id: Some(uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1")), + id: Some(folder_id), name: "test".to_string(), revision_date: "2025-01-01T00:00:00Z".parse().unwrap(), } ); + + // Confirm the folder was stored in the repository + assert_eq!( + store + .decrypt( + &repository + .get(folder_id.to_string()) + .await + .unwrap() + .unwrap() + ) + .unwrap(), + result + ); + } + + #[tokio::test] + async fn test_create_folder_http_error() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let (_server, api_config) = start_api_mock(vec![ + Mock::given(matchers::path("/folders")).respond_with(ResponseTemplate::new(500)) + ]) + .await; + + let repository = MemoryRepository::::default(); + + let result = create_folder( + &store, + &api_config, + &repository, + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), CreateFolderError::Api(_))); } } diff --git a/crates/bitwarden-vault/src/folder/folder_edit.rs b/crates/bitwarden-vault/src/folder/edit.rs similarity index 61% rename from crates/bitwarden-vault/src/folder/folder_edit.rs rename to crates/bitwarden-vault/src/folder/edit.rs index ec40a1d36..74a3d6614 100644 --- a/crates/bitwarden-vault/src/folder/folder_edit.rs +++ b/crates/bitwarden-vault/src/folder/edit.rs @@ -9,10 +9,17 @@ use wasm_bindgen::prelude::*; use crate::{Folder, FolderAddEditRequest, FolderView, VaultParseError}; +/// Item does not already exist error. +#[derive(Debug, thiserror::Error)] +#[error("Item does not already exist")] +pub struct ItemDoesNotExistError; + #[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, Error)] pub enum EditFolderError { + #[error(transparent)] + ItemDoesNotExist(#[from] ItemDoesNotExistError), #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] @@ -38,7 +45,7 @@ pub(super) async fn edit_folder + ?Sized>( repository .get(folder_id.to_owned()) .await? - .ok_or(MissingFieldError("Folder not found in repository"))?; + .ok_or(ItemDoesNotExistError)?; let folder_request = key_store.encrypt(request)?; @@ -57,7 +64,10 @@ pub(super) async fn edit_folder + ?Sized>( #[cfg(test)] mod tests { - use bitwarden_api_api::models::{FolderRequestModel, FolderResponseModel}; + use bitwarden_api_api::{ + apis::configuration::Configuration, + models::{FolderRequestModel, FolderResponseModel}, + }; use bitwarden_core::key_management::SymmetricKeyId; use bitwarden_crypto::{PrimitiveEncryptable, SymmetricCryptoKey}; use bitwarden_test::{start_api_mock, MemoryRepository}; @@ -66,6 +76,27 @@ mod tests { use super::*; + async fn repository_add_folder( + repository: &MemoryRepository, + store: &KeyStore, + folder_id: uuid::Uuid, + name: &str, + ) { + repository + .set( + folder_id.to_string(), + Folder { + id: Some(folder_id), + name: name + .encrypt(&mut store.context(), SymmetricKeyId::User) + .unwrap(), + revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), + }, + ) + .await + .unwrap(); + } + #[tokio::test] async fn test_edit_folder() { let store: KeyStore = KeyStore::default(); @@ -94,20 +125,7 @@ mod tests { .await; let repository = MemoryRepository::::default(); - - repository - .set( - folder_id.to_string(), - Folder { - id: Some(folder_id), - name: "old_name" - .encrypt(&mut store.context(), SymmetricKeyId::User) - .unwrap(), - revision_date: "2024-01-01T00:00:00Z".parse().unwrap(), - }, - ) - .await - .unwrap(); + repository_add_folder(&repository, &store, folder_id, "old_name").await; let result = edit_folder( &store, @@ -130,4 +148,65 @@ mod tests { } ); } + + #[tokio::test] + async fn test_edit_folder_does_not_exist() { + let store: KeyStore = KeyStore::default(); + + let repository = MemoryRepository::::default(); + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let result = edit_folder( + &store, + &Configuration::default(), + &repository, + &folder_id.to_string(), + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + EditFolderError::ItemDoesNotExist(_) + )); + } + + #[tokio::test] + async fn test_edit_folder_http_error() { + let store: KeyStore = KeyStore::default(); + #[allow(deprecated)] + let _ = store.context_mut().set_symmetric_key( + SymmetricKeyId::User, + SymmetricCryptoKey::make_aes256_cbc_hmac_key(), + ); + + let folder_id = uuid!("25afb11c-9c95-4db5-8bac-c21cb204a3f1"); + + let (_server, api_config) = start_api_mock(vec![Mock::given(matchers::path(format!( + "/folders/{}", + folder_id + ))) + .respond_with(ResponseTemplate::new(500))]) + .await; + + let repository = MemoryRepository::::default(); + repository_add_folder(&repository, &store, folder_id, "old_name").await; + + let result = edit_folder( + &store, + &api_config, + &repository, + &folder_id.to_string(), + FolderAddEditRequest { + name: "test".to_string(), + }, + ) + .await; + + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), EditFolderError::Api(_))); + } } diff --git a/crates/bitwarden-vault/src/folder/mod.rs b/crates/bitwarden-vault/src/folder/mod.rs index aad88ed39..33115eb53 100644 --- a/crates/bitwarden-vault/src/folder/mod.rs +++ b/crates/bitwarden-vault/src/folder/mod.rs @@ -1,9 +1,9 @@ +mod create; +mod edit; mod folder_client; -mod folder_create; -mod folder_edit; mod folder_models; +pub use create::*; +pub use edit::*; pub use folder_client::*; -pub use folder_create::*; -pub use folder_edit::*; pub use folder_models::*; From a4f489f5df5c497b4ca11aee53bb2faa13e80335 Mon Sep 17 00:00:00 2001 From: Hinton Date: Mon, 7 Jul 2025 11:24:00 +0200 Subject: [PATCH 25/28] Resolve review feedback --- crates/bitwarden-vault/src/folder/edit.rs | 6 ++++-- crates/bitwarden-vault/src/folder/folder_client.rs | 13 +++++++------ 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bitwarden-vault/src/folder/edit.rs b/crates/bitwarden-vault/src/folder/edit.rs index 74a3d6614..412b9832c 100644 --- a/crates/bitwarden-vault/src/folder/edit.rs +++ b/crates/bitwarden-vault/src/folder/edit.rs @@ -1,5 +1,5 @@ use bitwarden_api_api::apis::folders_api; -use bitwarden_core::{key_management::KeyIds, require, ApiError, MissingFieldError}; +use bitwarden_core::{key_management::KeyIds, ApiError, MissingFieldError}; use bitwarden_crypto::{CryptoError, KeyStore}; use bitwarden_error::bitwarden_error; use bitwarden_state::repository::{Repository, RepositoryError}; @@ -55,8 +55,10 @@ pub(super) async fn edit_folder + ?Sized>( let folder: Folder = resp.try_into()?; + debug_assert!(folder.id.unwrap_or_default().to_string() == folder_id); + repository - .set(require!(folder.id).to_string(), folder.clone()) + .set(folder_id.to_string(), folder.clone()) .await?; Ok(key_store.decrypt(&folder)?) diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs index 5fd6adef9..08c767722 100644 --- a/crates/bitwarden-vault/src/folder/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -11,7 +11,7 @@ use crate::{ CreateFolderError, EditFolderError, Folder, FolderAddEditRequest, FolderView, }; -#[allow(missing_docs)] +/// Wrapper for folder specific functionality. #[cfg_attr(feature = "wasm", wasm_bindgen)] pub struct FoldersClient { pub(crate) client: Client, @@ -19,28 +19,28 @@ pub struct FoldersClient { #[cfg_attr(feature = "wasm", wasm_bindgen)] impl FoldersClient { - #[allow(missing_docs)] + /// Encrypt a [FolderView] to a [Folder]. pub fn encrypt(&self, folder_view: FolderView) -> Result { let key_store = self.client.internal.get_key_store(); let folder = key_store.encrypt(folder_view)?; Ok(folder) } - #[allow(missing_docs)] + /// Encrypt a [Folder] to [FolderView]. pub fn decrypt(&self, folder: Folder) -> Result { let key_store = self.client.internal.get_key_store(); let folder_view = key_store.decrypt(&folder)?; Ok(folder_view) } - #[allow(missing_docs)] + /// Decrypt a list of [Folder]s to a list of [FolderView]s. pub fn decrypt_list(&self, folders: Vec) -> Result, DecryptError> { let key_store = self.client.internal.get_key_store(); let views = key_store.decrypt_list(&folders)?; Ok(views) } - /// Create a new folder and save it to the server. + /// Create a new [Folder] and save it to the server. pub async fn create( &self, request: FolderAddEditRequest, @@ -52,7 +52,7 @@ impl FoldersClient { create_folder(key_store, &config.api, repository.as_ref(), request).await } - /// Edit the folder and save it to the server. + /// Edit the [Folder] and save it to the server. pub async fn edit( &self, folder_id: &str, @@ -74,6 +74,7 @@ impl FoldersClient { } impl FoldersClient { + /// Helper for getting the repository for folders. fn get_repository(&self) -> Result>, RepositoryError> { Ok(self .client From e00c6d95e675640cb2077b78ab551524522326fc Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 11 Jul 2025 12:17:43 +0200 Subject: [PATCH 26/28] Add list and get --- crates/bitwarden-vault/src/folder/edit.rs | 13 ++---- .../src/folder/folder_client.rs | 20 ++++++++- .../src/folder/folder_models.rs | 8 ++++ crates/bitwarden-vault/src/folder/get_list.rs | 41 +++++++++++++++++++ crates/bitwarden-vault/src/folder/mod.rs | 7 ++++ 5 files changed, 78 insertions(+), 11 deletions(-) create mode 100644 crates/bitwarden-vault/src/folder/get_list.rs diff --git a/crates/bitwarden-vault/src/folder/edit.rs b/crates/bitwarden-vault/src/folder/edit.rs index 412b9832c..d93c637fd 100644 --- a/crates/bitwarden-vault/src/folder/edit.rs +++ b/crates/bitwarden-vault/src/folder/edit.rs @@ -7,19 +7,14 @@ use thiserror::Error; #[cfg(feature = "wasm")] use wasm_bindgen::prelude::*; -use crate::{Folder, FolderAddEditRequest, FolderView, VaultParseError}; - -/// Item does not already exist error. -#[derive(Debug, thiserror::Error)] -#[error("Item does not already exist")] -pub struct ItemDoesNotExistError; +use crate::{Folder, FolderAddEditRequest, FolderView, ItemNotFoundError, VaultParseError}; #[allow(missing_docs)] #[bitwarden_error(flat)] #[derive(Debug, Error)] pub enum EditFolderError { #[error(transparent)] - ItemDoesNotExist(#[from] ItemDoesNotExistError), + ItemNotFound(#[from] ItemNotFoundError), #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] @@ -45,7 +40,7 @@ pub(super) async fn edit_folder + ?Sized>( repository .get(folder_id.to_owned()) .await? - .ok_or(ItemDoesNotExistError)?; + .ok_or(ItemNotFoundError)?; let folder_request = key_store.encrypt(request)?; @@ -172,7 +167,7 @@ mod tests { assert!(result.is_err()); assert!(matches!( result.unwrap_err(), - EditFolderError::ItemDoesNotExist(_) + EditFolderError::ItemNotFound(_) )); } diff --git a/crates/bitwarden-vault/src/folder/folder_client.rs b/crates/bitwarden-vault/src/folder/folder_client.rs index 08c767722..63370bab6 100644 --- a/crates/bitwarden-vault/src/folder/folder_client.rs +++ b/crates/bitwarden-vault/src/folder/folder_client.rs @@ -7,8 +7,8 @@ use wasm_bindgen::prelude::*; use crate::{ error::{DecryptError, EncryptError}, - folder::{create_folder, edit_folder}, - CreateFolderError, EditFolderError, Folder, FolderAddEditRequest, FolderView, + folder::{create_folder, edit_folder, get_folder, list_folders}, + CreateFolderError, EditFolderError, Folder, FolderAddEditRequest, FolderView, GetFolderError, }; /// Wrapper for folder specific functionality. @@ -40,6 +40,22 @@ impl FoldersClient { Ok(views) } + /// Get all folders from state and decrypt them to a list of [FolderView]. + pub async fn list(&self) -> Result, GetFolderError> { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + list_folders(key_store, repository.as_ref()).await + } + + /// Get a specific [Folder] by its ID from state and decrypt it to a [FolderView]. + pub async fn get(&self, folder_id: &str) -> Result { + let key_store = self.client.internal.get_key_store(); + let repository = self.get_repository()?; + + get_folder(key_store, repository.as_ref(), folder_id).await + } + /// Create a new [Folder] and save it to the server. pub async fn create( &self, diff --git a/crates/bitwarden-vault/src/folder/folder_models.rs b/crates/bitwarden-vault/src/folder/folder_models.rs index 070b17e84..c9c298a85 100644 --- a/crates/bitwarden-vault/src/folder/folder_models.rs +++ b/crates/bitwarden-vault/src/folder/folder_models.rs @@ -39,6 +39,14 @@ pub struct FolderView { pub revision_date: DateTime, } +impl wasm_bindgen::__rt::VectorIntoJsValue for FolderView { + fn vector_into_jsvalue( + vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>, + ) -> wasm_bindgen::JsValue { + wasm_bindgen::__rt::js_value_vector_into_jsvalue(vector) + } +} + impl IdentifyKey for Folder { fn key_identifier(&self) -> SymmetricKeyId { SymmetricKeyId::User diff --git a/crates/bitwarden-vault/src/folder/get_list.rs b/crates/bitwarden-vault/src/folder/get_list.rs new file mode 100644 index 000000000..6884ca162 --- /dev/null +++ b/crates/bitwarden-vault/src/folder/get_list.rs @@ -0,0 +1,41 @@ +use bitwarden_core::key_management::KeyIds; +use bitwarden_crypto::{CryptoError, KeyStore}; +use bitwarden_error::bitwarden_error; +use bitwarden_state::repository::{Repository, RepositoryError}; +use thiserror::Error; + +use crate::{Folder, FolderView, ItemNotFoundError}; + +#[allow(missing_docs)] +#[bitwarden_error(flat)] +#[derive(Debug, Error)] +pub enum GetFolderError { + #[error(transparent)] + ItemDoesNotExist(#[from] ItemNotFoundError), + #[error(transparent)] + Crypto(#[from] CryptoError), + #[error(transparent)] + RepositoryError(#[from] RepositoryError), +} + +pub(super) async fn get_folder( + store: &KeyStore, + repository: &dyn Repository, + id: &str, +) -> Result { + let folder = repository + .get(id.to_string()) + .await? + .ok_or(ItemNotFoundError)?; + + Ok(store.decrypt(&folder)?) +} + +pub(super) async fn list_folders( + store: &KeyStore, + repository: &dyn Repository, +) -> Result, GetFolderError> { + let folders = repository.list().await?; + let views = store.decrypt_list(&folders)?; + Ok(views) +} diff --git a/crates/bitwarden-vault/src/folder/mod.rs b/crates/bitwarden-vault/src/folder/mod.rs index 33115eb53..13fb151ef 100644 --- a/crates/bitwarden-vault/src/folder/mod.rs +++ b/crates/bitwarden-vault/src/folder/mod.rs @@ -2,8 +2,15 @@ mod create; mod edit; mod folder_client; mod folder_models; +mod get_list; pub use create::*; pub use edit::*; pub use folder_client::*; pub use folder_models::*; +pub use get_list::*; + +/// Item does not exist error. +#[derive(Debug, thiserror::Error)] +#[error("Item does not exist")] +pub struct ItemNotFoundError; From 11e0ecf1b082f3a1a077638291886d441e123555 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 11 Jul 2025 12:21:06 +0200 Subject: [PATCH 27/28] Rename error --- crates/bitwarden-vault/src/folder/get_list.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bitwarden-vault/src/folder/get_list.rs b/crates/bitwarden-vault/src/folder/get_list.rs index 6884ca162..65ac6b2fa 100644 --- a/crates/bitwarden-vault/src/folder/get_list.rs +++ b/crates/bitwarden-vault/src/folder/get_list.rs @@ -11,7 +11,7 @@ use crate::{Folder, FolderView, ItemNotFoundError}; #[derive(Debug, Error)] pub enum GetFolderError { #[error(transparent)] - ItemDoesNotExist(#[from] ItemNotFoundError), + ItemNotFound(#[from] ItemNotFoundError), #[error(transparent)] Crypto(#[from] CryptoError), #[error(transparent)] From 646d2220db1124aacdec79bba75ad5bab8881532 Mon Sep 17 00:00:00 2001 From: Hinton Date: Fri, 11 Jul 2025 12:41:54 +0200 Subject: [PATCH 28/28] Properly cfg flag wasm --- crates/bitwarden-vault/src/folder/folder_models.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bitwarden-vault/src/folder/folder_models.rs b/crates/bitwarden-vault/src/folder/folder_models.rs index c9c298a85..0da5af72f 100644 --- a/crates/bitwarden-vault/src/folder/folder_models.rs +++ b/crates/bitwarden-vault/src/folder/folder_models.rs @@ -39,6 +39,7 @@ pub struct FolderView { pub revision_date: DateTime, } +#[cfg(feature = "wasm")] impl wasm_bindgen::__rt::VectorIntoJsValue for FolderView { fn vector_into_jsvalue( vector: wasm_bindgen::__rt::std::boxed::Box<[Self]>,