Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ bitwarden-send = { path = "crates/bitwarden-send", 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-state-migrations = { path = "crates/bitwarden-state-migrations", 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-uniffi-error = { path = "crates/bitwarden-uniffi-error", version = "=1.0.0" }
Expand Down
6 changes: 3 additions & 3 deletions crates/bitwarden-core/src/platform/state_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::sync::Arc;

use bitwarden_state::{
registry::{RepositoryNotFoundError, StateRegistryError},
repository::{Repository, RepositoryItem, RepositoryItemData},
repository::{Repository, RepositoryItem, RepositoryMigrations},
DatabaseConfiguration,
};

Expand Down Expand Up @@ -36,12 +36,12 @@ impl StateClient {
pub async fn initialize_database(
&self,
configuration: DatabaseConfiguration,
repositories: Vec<RepositoryItemData>,
migrations: RepositoryMigrations,
) -> Result<(), StateRegistryError> {
self.client
.internal
.repository_map
.initialize_database(configuration, repositories)
.initialize_database(configuration, migrations)
.await
}

Expand Down
21 changes: 21 additions & 0 deletions crates/bitwarden-state-migrations/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "bitwarden-state-migrations"
version.workspace = true
authors.workspace = true
edition.workspace = true
rust-version.workspace = true
homepage.workspace = true
repository.workspace = true
license-file.workspace = true
keywords.workspace = true

[features]
uniffi = ["bitwarden-vault/uniffi"]
wasm = ["bitwarden-vault/wasm"]

[dependencies]
bitwarden-state = { workspace = true }
bitwarden-vault = { workspace = true }

[lints]
workspace = true
4 changes: 4 additions & 0 deletions crates/bitwarden-state-migrations/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# bitwarden-state-migrations

This crate contains migrations for the SDK-managed state framework. It should only be imported by
the final application crates (`bitwarden-wasm-internal` and `bitwarden-uniffi`)
15 changes: 15 additions & 0 deletions crates/bitwarden-state-migrations/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
#![doc = include_str!("../README.md")]

use bitwarden_state::repository::{RepositoryItem, RepositoryMigrationStep, RepositoryMigrations};
use bitwarden_vault::{Cipher, Folder};

/// Returns a list of all SDK-managed repository migrations.
pub fn get_sdk_managed_migrations() -> RepositoryMigrations {
use RepositoryMigrationStep::*;
RepositoryMigrations::new(vec![
// Add any new migrations here. Note that order matters, and that removing a repository
// requires a separate migration step using `Remove(...)`.
Add(Cipher::data()),
Add(Folder::data()),
])
}
72 changes: 15 additions & 57 deletions crates/bitwarden-state/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,65 +182,23 @@ getClient(userId = userId).platform().store().registerCipherStore(CipherStoreImp

With `SDK-Managed State`, the SDK will be exclusively responsible for the data storage. This means
that the clients don't need to make any changes themselves, as the implementation is internal to the
SDK. To add support for an SDK managed `Repository`, it needs to be added to the initialization code
for WASM and UniFFI. This example shows how to add support for `Cipher`s.
SDK. To add support for an SDK managed `Repository`, a new migration step needs to be added to the
`bitwarden-state-migrations` crate.

### How to initialize SDK-Managed State on WASM
### How to initialize SDK-Managed State

Go to `crates/bitwarden-wasm-internal/src/platform/mod.rs` and add a line with your type, as shown
below. In this example we're registering `Cipher` as both client and SDK managed to show how both
are done, but you can also just do one or the other.
Go to `crates/bitwarden-state-migrations/src/lib.rs` and add a line with your type, as shown below.
In this example we're registering `Cipher` and `Folder` as SDK managed.

```rust,ignore
pub async fn initialize_state(
&self,
cipher_repository: CipherRepository,
) -> Result<(), bitwarden_state::registry::StateRegistryError> {
let cipher = cipher_repository.into_channel_impl();
// Register the provided repository as client managed state
self.0.platform().state().register_client_managed(cipher);

let sdk_managed_repositories = vec![
// This should list all the SDK-managed repositories
<Cipher as RepositoryItem>::data(),
// Add your type here
];

self.0
.platform()
.state()
.initialize_database(sdk_managed_repositories)
.await
}
```

### How to initialize SDK-Managed State on UniFFI

Go to `crates/bitwarden-uniffi/src/platform/mod.rs` and add a line with your type, as shown below.
In this example we're registering `Cipher` as both client and SDK managed to show how both are done,
but you can also just do one or the other.

```rust,ignore
pub async fn initialize_state(
&self,
cipher_repository: Arc<dyn CipherRepository>,
) -> Result<()> {
let cipher = UniffiRepositoryBridge::new(cipher_repository);
// Register the provided repository as client managed state
self.0.platform().state().register_client_managed(cipher);

let sdk_managed_repositories = vec![
// This should list all the SDK-managed repositories
<Cipher as RepositoryItem>::data(),
// Add your type here
];

self.0
.platform()
.state()
.initialize_database(sdk_managed_repositories)
.await
.map_err(Error::StateRegistry)?;
Ok(())
}
/// Returns a list of all SDK-managed repository migrations.
pub fn get_sdk_managed_migrations() -> RepositoryMigrations {
use RepositoryMigrationStep::*;
RepositoryMigrations::new(vec![
// Add any new migrations here. Note that order matters, and that removing a repository
// requires a separate migration step using `Remove(...)`.
Add(Cipher::data()),
Add(Folder::data()),
])
}
```
8 changes: 4 additions & 4 deletions crates/bitwarden-state/src/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use serde::{de::DeserializeOwned, Serialize};
use thiserror::Error;

use crate::{
repository::{Repository, RepositoryItem, RepositoryItemData},
repository::{Repository, RepositoryItem, RepositoryItemData, RepositoryMigrations},
sdk_managed::{Database, DatabaseConfiguration, SystemDatabase},
};

Expand Down Expand Up @@ -71,19 +71,19 @@ impl StateRegistry {
pub async fn initialize_database(
&self,
configuration: DatabaseConfiguration,
repositories: Vec<RepositoryItemData>,
migrations: RepositoryMigrations,
) -> Result<(), StateRegistryError> {
if self.database.get().is_some() {
return Err(StateRegistryError::DatabaseAlreadyInitialized);
}
let _ = self
.database
.set(SystemDatabase::initialize(configuration, &repositories).await?);
.set(SystemDatabase::initialize(configuration, migrations.clone()).await?);

*self
.sdk_managed
.write()
.expect("RwLock should not be poisoned") = repositories.clone();
.expect("RwLock should not be poisoned") = migrations.into_repository_items();

Ok(())
}
Expand Down
45 changes: 45 additions & 0 deletions crates/bitwarden-state/src/repository.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,51 @@ pub const fn validate_registry_name(name: &str) -> bool {
true
}

/// Represents a set of migrations for multiple repositories in a database migration process.
#[derive(Debug, Clone)]
pub struct RepositoryMigrations {
pub(crate) steps: Vec<RepositoryMigrationStep>,
// This is used only by indexedDB
#[allow(dead_code)]
pub(crate) version: u32,
}

/// Represents a single step for a repository in a database migration process.
#[derive(Debug, Clone, Copy)]
pub enum RepositoryMigrationStep {
/// Add a new repository.
Add(RepositoryItemData),
/// Remove an existing repository.
Remove(RepositoryItemData),
}

impl RepositoryMigrations {
/// Create a new `RepositoryMigrations` with the given steps. The version is derived from the
/// number of steps.
pub fn new(steps: Vec<RepositoryMigrationStep>) -> Self {
Self {
version: steps.len() as u32,
steps,
}
}

/// Converts the migration steps into a list of unique repository item data.
pub fn into_repository_items(self) -> Vec<RepositoryItemData> {
let mut map = std::collections::HashMap::new();
for step in self.steps {
match step {
RepositoryMigrationStep::Add(data) => {
map.insert(data.type_id, data);
}
RepositoryMigrationStep::Remove(data) => {
map.remove(&data.type_id);
}
}
}
map.into_values().collect()
}
}

/// Register a type for use in a repository. The type must only be registered once in the crate
/// where it's defined. The provided name must be unique and not be changed.
#[macro_export]
Expand Down
28 changes: 17 additions & 11 deletions crates/bitwarden-state/src/sdk_managed/indexed_db.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
use indexed_db::Error;
use js_sys::JsString;
use serde::{de::DeserializeOwned, ser::Serialize};

use crate::{
repository::{RepositoryItem, RepositoryItemData},
repository::{RepositoryItem, RepositoryMigrationStep, RepositoryMigrations},
sdk_managed::{Database, DatabaseConfiguration, DatabaseError},
};

Expand All @@ -22,27 +23,32 @@ pub struct IndexedDbDatabase(
impl Database for IndexedDbDatabase {
async fn initialize(
configuration: DatabaseConfiguration,
registrations: &[RepositoryItemData],
migrations: RepositoryMigrations,
) -> Result<Self, DatabaseError> {
let DatabaseConfiguration::IndexedDb { db_name } = configuration else {
return Err(DatabaseError::UnsupportedConfiguration(configuration));
};

let factory = indexed_db::Factory::get()?;

let registrations = registrations.to_vec();

// TODO: This version will be replaced by a proper migration system in a followup PR:
// https://github.com/bitwarden/sdk-internal/pull/410
let version: u32 = 1;

// Open the database, creating it if needed
let db = factory
.open(&db_name, version, async move |evt| {
.open(&db_name, migrations.version, async move |evt| {
let db = evt.database();

for reg in registrations {
db.build_object_store(reg.name()).create()?;
for step in &migrations.steps {
match step {
RepositoryMigrationStep::Add(data) => {
db.build_object_store(data.name()).create()?;
}
RepositoryMigrationStep::Remove(data) => {
match db.delete_object_store(data.name()) {
// If the store doesn't exist, we can ignore the error
Ok(_) | Err(Error::DoesNotExist) => {}
Err(e) => return Err(e),
}
}
}
}

Ok(())
Expand Down
4 changes: 2 additions & 2 deletions crates/bitwarden-state/src/sdk_managed/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use bitwarden_error::bitwarden_error;
use serde::{de::DeserializeOwned, ser::Serialize};
use thiserror::Error;

use crate::repository::{Repository, RepositoryError, RepositoryItem, RepositoryItemData};
use crate::repository::{Repository, RepositoryError, RepositoryItem, RepositoryMigrations};

mod configuration;
pub use configuration::DatabaseConfiguration;
Expand Down Expand Up @@ -43,7 +43,7 @@ pub enum DatabaseError {
pub trait Database {
async fn initialize(
configuration: DatabaseConfiguration,
registrations: &[RepositoryItemData],
registrations: RepositoryMigrations,
) -> Result<Self, DatabaseError>
where
Self: Sized;
Expand Down
Loading
Loading