Skip to content

Commit 4ab53f4

Browse files
committed
Client-managed validation
1 parent 546737a commit 4ab53f4

File tree

17 files changed

+399
-138
lines changed

17 files changed

+399
-138
lines changed

Cargo.lock

Lines changed: 15 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-core/src/client/client.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::sync::{Arc, OnceLock, RwLock};
22

33
use bitwarden_crypto::KeyStore;
44
#[cfg(feature = "internal")]
5-
use bitwarden_state::repository::RepositoryMap;
5+
use bitwarden_state::registry::StateRegistry;
66
use reqwest::header::{self, HeaderValue};
77

88
use super::internal::InternalClient;
@@ -91,7 +91,7 @@ impl Client {
9191
external_client,
9292
key_store: KeyStore::default(),
9393
#[cfg(feature = "internal")]
94-
repository_map: RwLock::new(RepositoryMap::default()),
94+
repository_map: RwLock::new(StateRegistry::new()),
9595
}),
9696
}
9797
}

crates/bitwarden-core/src/client/internal.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use bitwarden_crypto::SymmetricCryptoKey;
66
#[cfg(feature = "internal")]
77
use bitwarden_crypto::{EncString, Kdf, MasterKey, PinKey, UnsignedSharedKey};
88
#[cfg(feature = "internal")]
9-
use bitwarden_state::repository::RepositoryMap;
9+
use bitwarden_state::registry::StateRegistry;
1010
use chrono::Utc;
1111
use uuid::Uuid;
1212

@@ -67,7 +67,7 @@ pub struct InternalClient {
6767
pub(super) key_store: KeyStore<KeyIds>,
6868

6969
#[cfg(feature = "internal")]
70-
pub(crate) repository_map: RwLock<RepositoryMap>,
70+
pub(crate) repository_map: RwLock<StateRegistry>,
7171
}
7272

7373
impl InternalClient {
Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
use std::sync::Arc;
22

3-
use bitwarden_state::repository::Repository;
3+
use bitwarden_state::{
4+
registry::StateRegistryError,
5+
repository::{Repository, RepositoryItem},
6+
};
47

58
use crate::Client;
69

@@ -11,22 +14,25 @@ pub struct StateClient {
1114

1215
impl StateClient {
1316
/// Register a client managed state repository for a specific type.
14-
pub fn register_repository<T: 'static + Repository<V>, V: 'static>(&self, store: Arc<T>) {
17+
pub fn register_client_managed<T: 'static + Repository<V>, V: RepositoryItem>(
18+
&self,
19+
store: Arc<T>,
20+
) -> Result<(), StateRegistryError> {
1521
self.client
1622
.internal
1723
.repository_map
1824
.write()
1925
.expect("RwLock is not poisoned")
20-
.insert(store);
26+
.register_client_managed(store)
2127
}
2228

2329
/// Get a client managed state repository for a specific type, if it exists.
24-
pub fn get_repository<T: 'static>(&self) -> Option<Arc<dyn Repository<T>>> {
30+
pub fn get_client_managed<T: RepositoryItem>(&self) -> Option<Arc<dyn Repository<T>>> {
2531
self.client
2632
.internal
2733
.repository_map
2834
.read()
2935
.expect("RwLock is not poisoned")
30-
.get()
36+
.get_client_managed()
3137
}
3238
}

crates/bitwarden-state/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ wasm = []
1616
[dependencies]
1717
async-trait = { workspace = true }
1818
bitwarden-error = { workspace = true }
19+
inventory = "0.3.20"
1920
log = { workspace = true }
2021
serde = { workspace = true }
2122
thiserror = { workspace = true }

crates/bitwarden-state/src/lib.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@
22

33
/// This module provides a generic repository interface for storing and retrieving items.
44
pub mod repository;
5+
6+
/// This module provides a registry for managing repositories of different types.
7+
pub mod registry;
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
use std::{
2+
any::{Any, TypeId},
3+
collections::HashMap,
4+
sync::Arc,
5+
};
6+
7+
use bitwarden_error::bitwarden_error;
8+
use thiserror::Error;
9+
10+
use super::repository::RepositoryItemRegistration;
11+
use crate::repository::{Repository, RepositoryItem};
12+
13+
/// A registry that contains repositories for different types of items.
14+
/// These repositories can be either managed by the client or by the SDK itself.
15+
pub struct StateRegistry {
16+
client_managed: HashMap<TypeId, Box<dyn Any + Send + Sync>>,
17+
}
18+
19+
impl std::fmt::Debug for StateRegistry {
20+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21+
f.debug_struct("StateRegistry")
22+
.field("client_managed", &self.client_managed.keys())
23+
.finish()
24+
}
25+
}
26+
27+
#[allow(missing_docs)]
28+
#[bitwarden_error(flat)]
29+
#[derive(Debug, Error)]
30+
pub enum StateRegistryError {
31+
#[error("Repository for type {0} is not registered as client-managed")]
32+
RepositoryNotClientManaged(&'static str),
33+
34+
#[error("Not all client-managed repositories are registered")]
35+
NotAllRepositoriesRegistered,
36+
}
37+
38+
impl StateRegistry {
39+
/// Creates a new empty `StateRegistry`.
40+
#[allow(clippy::new_without_default)]
41+
pub fn new() -> Self {
42+
StateRegistry {
43+
client_managed: HashMap::new(),
44+
}
45+
}
46+
47+
/// Registers a client-managed repository into the map, associating it with its type.
48+
pub fn register_client_managed<T: RepositoryItem>(
49+
&mut self,
50+
value: Arc<dyn Repository<T>>,
51+
) -> Result<(), StateRegistryError> {
52+
let mut possible_registrations = RepositoryItemRegistration::iter();
53+
match possible_registrations.find(|reg| reg.type_id() == TypeId::of::<T>()) {
54+
Some(reg) => {
55+
if !reg.rtype.is_client_managed() {
56+
return Err(StateRegistryError::RepositoryNotClientManaged(reg.name));
57+
}
58+
}
59+
// This should never happen, as we have tests to ensure all repositories are registered.
60+
_ => {
61+
return Err(StateRegistryError::NotAllRepositoriesRegistered);
62+
}
63+
}
64+
65+
self.client_managed
66+
.insert(TypeId::of::<T>(), Box::new(value));
67+
68+
Ok(())
69+
}
70+
71+
/// Retrieves a client-managed repository from the map given its type.
72+
pub fn get_client_managed<T: RepositoryItem>(&self) -> Option<Arc<dyn Repository<T>>> {
73+
self.client_managed
74+
.get(&TypeId::of::<T>())
75+
.and_then(|boxed| boxed.downcast_ref::<Arc<dyn Repository<T>>>())
76+
.map(Arc::clone)
77+
}
78+
79+
/// Validates that all repositories registered in the client-managed state registry.
80+
/// This should only be called after all the repositories have been registered by the clients.
81+
pub fn validate_repositories(&self) -> Result<(), StateRegistryError> {
82+
let possible_registrations = RepositoryItemRegistration::iter();
83+
let mut missing_repository = false;
84+
85+
for reg in possible_registrations {
86+
if reg.rtype.is_client_managed() && !self.client_managed.contains_key(&reg.type_id()) {
87+
log::error!(
88+
"Repository for type {} is not registered in the client-managed state registry",
89+
reg.name
90+
);
91+
missing_repository = true;
92+
}
93+
}
94+
95+
if missing_repository {
96+
return Err(StateRegistryError::NotAllRepositoriesRegistered);
97+
}
98+
99+
Ok(())
100+
}
101+
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
use crate::{
107+
register_repository_item,
108+
repository::{RepositoryError, RepositoryItem},
109+
};
110+
111+
macro_rules! impl_repository {
112+
($name:ident, $ty:ty) => {
113+
#[async_trait::async_trait]
114+
impl Repository<$ty> for $name {
115+
async fn get(&self, _key: String) -> Result<Option<$ty>, RepositoryError> {
116+
Ok(Some(TestItem(self.0.clone())))
117+
}
118+
async fn list(&self) -> Result<Vec<$ty>, RepositoryError> {
119+
unimplemented!()
120+
}
121+
async fn set(&self, _key: String, _value: $ty) -> Result<(), RepositoryError> {
122+
unimplemented!()
123+
}
124+
async fn remove(&self, _key: String) -> Result<(), RepositoryError> {
125+
unimplemented!()
126+
}
127+
}
128+
};
129+
}
130+
131+
#[derive(PartialEq, Eq, Debug)]
132+
struct TestA(usize);
133+
#[derive(PartialEq, Eq, Debug)]
134+
struct TestB(String);
135+
#[derive(PartialEq, Eq, Debug)]
136+
struct TestC(Vec<u8>);
137+
#[derive(PartialEq, Eq, Debug)]
138+
struct TestItem<T>(T);
139+
140+
register_repository_item!(TestItem<usize>, "TestItem<usize>", ClientManaged);
141+
register_repository_item!(TestItem<String>, "TestItem<String>", ClientManaged);
142+
register_repository_item!(TestItem<Vec<u8>>, "TestItem<Vec<u8>>", ClientManaged);
143+
144+
impl_repository!(TestA, TestItem<usize>);
145+
impl_repository!(TestB, TestItem<String>);
146+
impl_repository!(TestC, TestItem<Vec<u8>>);
147+
148+
#[tokio::test]
149+
async fn test_repository_map() {
150+
let a = Arc::new(TestA(145832));
151+
let b = Arc::new(TestB("test".to_string()));
152+
let c = Arc::new(TestC(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]));
153+
154+
let mut map = StateRegistry::new();
155+
156+
async fn get<T: RepositoryItem>(map: &StateRegistry) -> Option<T> {
157+
map.get_client_managed::<T>()
158+
.unwrap()
159+
.get(String::new())
160+
.await
161+
.unwrap()
162+
}
163+
164+
assert!(map.get_client_managed::<TestItem<usize>>().is_none());
165+
assert!(map.get_client_managed::<TestItem<String>>().is_none());
166+
assert!(map.get_client_managed::<TestItem<Vec<u8>>>().is_none());
167+
168+
map.register_client_managed(a.clone()).unwrap();
169+
assert_eq!(get(&map).await, Some(TestItem(a.0)));
170+
assert!(map.get_client_managed::<TestItem<String>>().is_none());
171+
assert!(map.get_client_managed::<TestItem<Vec<u8>>>().is_none());
172+
173+
map.register_client_managed(b.clone()).unwrap();
174+
assert_eq!(get(&map).await, Some(TestItem(a.0)));
175+
assert_eq!(get(&map).await, Some(TestItem(b.0.clone())));
176+
assert!(map.get_client_managed::<TestItem<Vec<u8>>>().is_none());
177+
178+
map.register_client_managed(c.clone()).unwrap();
179+
assert_eq!(get(&map).await, Some(TestItem(a.0)));
180+
assert_eq!(get(&map).await, Some(TestItem(b.0.clone())));
181+
assert_eq!(get(&map).await, Some(TestItem(c.0.clone())));
182+
}
183+
}

0 commit comments

Comments
 (0)