bitwarden_collections/
collection_client.rs1use std::collections::HashMap;
2
3use bitwarden_core::{Client, FromClient};
4use serde::{Deserialize, Serialize};
5
6use crate::{
7 collection::{Collection, CollectionId, CollectionView},
8 error::{CollectionDecryptError, CollectionEncryptError},
9 tree::{NodeItem, Tree},
10};
11
12#[derive(Serialize, Deserialize)]
18#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
19#[bitwarden_ffi::wasm_record]
20pub struct DecryptCollectionListResult {
21 pub successes: Vec<CollectionView>,
23 pub failures: Vec<Collection>,
25}
26
27#[allow(missing_docs)]
28#[bitwarden_ffi::wasm_object]
29#[derive(Clone)]
30pub struct CollectionsClient {
31 pub(crate) client: Client,
32}
33
34impl FromClient for CollectionsClient {
35 fn from_client(client: &Client) -> Self {
36 Self {
37 client: client.clone(),
38 }
39 }
40}
41
42#[bitwarden_ffi::wasm_export]
43impl CollectionsClient {
44 pub fn encrypt(
46 &self,
47 collection_view: CollectionView,
48 ) -> Result<Collection, CollectionEncryptError> {
49 let key_store = self.client.internal.get_key_store();
50 let collection = key_store.encrypt(collection_view)?;
51 Ok(collection)
52 }
53
54 pub fn encrypt_list(
57 &self,
58 collection_views: Vec<CollectionView>,
59 ) -> Result<Vec<Collection>, CollectionEncryptError> {
60 let key_store = self.client.internal.get_key_store();
61 let collections = key_store.encrypt_list(&collection_views)?;
62 Ok(collections)
63 }
64
65 #[allow(missing_docs)]
66 pub fn decrypt(
67 &self,
68 collection: Collection,
69 ) -> Result<CollectionView, CollectionDecryptError> {
70 let key_store = self.client.internal.get_key_store();
71 let view = key_store.decrypt(&collection)?;
72 Ok(view)
73 }
74
75 #[allow(missing_docs)]
76 pub fn decrypt_list(
77 &self,
78 collections: Vec<Collection>,
79 ) -> Result<Vec<CollectionView>, CollectionDecryptError> {
80 let key_store = self.client.internal.get_key_store();
81 let views = key_store.decrypt_list(&collections)?;
82 Ok(views)
83 }
84
85 pub fn decrypt_list_with_failures(
90 &self,
91 collections: Vec<Collection>,
92 ) -> DecryptCollectionListResult {
93 let key_store = self.client.internal.get_key_store();
94 let (successes, failures) = key_store.decrypt_list_with_failures(&collections);
95 DecryptCollectionListResult {
96 successes,
97 failures: failures.into_iter().cloned().collect(),
98 }
99 }
100
101 pub fn get_collection_tree(&self, collections: Vec<CollectionView>) -> CollectionViewTree {
105 CollectionViewTree {
106 tree: Tree::from_items(collections),
107 }
108 }
109}
110
111#[bitwarden_ffi::wasm_object]
112pub struct CollectionViewTree {
113 tree: Tree<CollectionView>,
114}
115
116#[bitwarden_ffi::wasm_object]
117pub struct CollectionViewNodeItem {
118 node_item: NodeItem<CollectionView>,
119}
120
121#[derive(Serialize, Deserialize)]
122#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
123#[bitwarden_ffi::wasm_record]
124pub struct AncestorMap {
125 pub ancestors: HashMap<CollectionId, String>,
126}
127
128#[bitwarden_ffi::wasm_export]
129impl CollectionViewNodeItem {
130 pub fn get_item(&self) -> CollectionView {
131 self.node_item.item.clone()
132 }
133
134 pub fn get_parent(&self) -> Option<CollectionView> {
135 self.node_item.parent.clone()
136 }
137
138 pub fn get_children(&self) -> Vec<CollectionView> {
139 self.node_item.children.clone()
140 }
141
142 pub fn get_ancestors(&self) -> AncestorMap {
143 AncestorMap {
144 ancestors: self
145 .node_item
146 .ancestors
147 .iter()
148 .map(|(&uuid, name)| (CollectionId::new(uuid), name.clone()))
149 .collect(),
150 }
151 }
152}
153
154#[bitwarden_ffi::wasm_export]
155impl CollectionViewTree {
156 pub fn get_item_for_view(
157 &self,
158 collection_view: CollectionView,
159 ) -> Option<CollectionViewNodeItem> {
160 self.tree
161 .get_item_by_id(collection_view.id.unwrap_or_default().into())
162 .map(|n| CollectionViewNodeItem { node_item: n })
163 }
164
165 pub fn get_root_items(&self) -> Vec<CollectionViewNodeItem> {
166 self.tree
167 .get_root_items()
168 .into_iter()
169 .map(|n| CollectionViewNodeItem { node_item: n })
170 .collect()
171 }
172
173 pub fn get_flat_items(&self) -> Vec<CollectionViewNodeItem> {
174 self.tree
175 .get_flat_items()
176 .into_iter()
177 .map(|n| CollectionViewNodeItem { node_item: n })
178 .collect()
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use bitwarden_core::{OrganizationId, client::test_accounts::test_bitwarden_com_account};
185
186 use super::*;
187 use crate::collection::CollectionType;
188
189 fn test_collection() -> Collection {
190 Collection {
191 id: Some("66c5ca57-0868-4c7e-902f-b181009709c0".parse().unwrap()),
192 organization_id: "1bc9ac1e-f5aa-45f2-94bf-b181009709b8".parse().unwrap(),
193 name: "2.EI9Km5BfrIqBa1W+WCccfA==|laWxNnx+9H3MZww4zm7cBSLisjpi81zreaQntRhegVI=|x42+qKFf5ga6DIL0OW5pxCdLrC/gm8CXJvf3UASGteI=".parse().unwrap(),
194 external_id: None,
195 hide_passwords: false,
196 read_only: false,
197 manage: false,
198 default_user_collection_email: None,
199 r#type: CollectionType::SharedCollection,
200 has_enabled_access_rule: None,
201 }
202 }
203
204 async fn test_collections_client() -> CollectionsClient {
205 let client = Client::init_test_account(test_bitwarden_com_account()).await;
206 CollectionsClient::from_client(&client)
207 }
208
209 #[tokio::test]
210 async fn test_decrypt_list() {
211 let collections = test_collections_client().await;
212
213 let dec = collections.decrypt_list(vec![test_collection()]).unwrap();
214
215 assert_eq!(dec[0].name, "Default collection");
216 }
217
218 #[tokio::test]
219 async fn test_decrypt() {
220 let collections = test_collections_client().await;
221
222 let dec = collections.decrypt(test_collection()).unwrap();
223
224 assert_eq!(dec.name, "Default collection");
225 }
226
227 #[tokio::test]
228 async fn test_decrypt_list_with_failures_all_success() {
229 let collections = test_collections_client().await;
230
231 let result = collections.decrypt_list_with_failures(vec![test_collection()]);
232
233 assert_eq!(result.successes.len(), 1);
234 assert!(result.failures.is_empty());
235 assert_eq!(result.successes[0].name, "Default collection");
236 }
237
238 #[tokio::test]
239 async fn test_decrypt_list_with_failures_mixed_results() {
240 let client = test_collections_client().await;
241
242 let valid_collection = test_collection();
243 let mut invalid_collection = test_collection();
244 invalid_collection.organization_id = OrganizationId::new_v4();
247
248 let collections = vec![valid_collection, invalid_collection.clone()];
249
250 let result = client.decrypt_list_with_failures(collections);
251
252 assert_eq!(result.successes.len(), 1);
253 assert_eq!(result.successes[0].name, "Default collection");
254
255 assert_eq!(result.failures.len(), 1);
256 assert_eq!(result.failures[0].id, invalid_collection.id);
259 assert_eq!(result.failures[0].name, invalid_collection.name);
260 }
261
262 #[tokio::test]
263 async fn test_decrypt_list_with_failures_empty_list() {
264 let collections = test_collections_client().await;
265
266 let result = collections.decrypt_list_with_failures(vec![]);
267
268 assert!(result.successes.is_empty());
269 assert!(result.failures.is_empty());
270 }
271
272 #[tokio::test]
273 async fn test_encrypt_decrypt_roundtrip() {
274 let collections = test_collections_client().await;
275
276 let view = collections.decrypt(test_collection()).unwrap();
277
278 assert_eq!(view.name, "Default collection");
279
280 let expected_id = view.id;
282 let expected_org_id = view.organization_id;
283 let re_encrypted = collections.encrypt(view).unwrap();
284 let re_decrypted = collections.decrypt(re_encrypted).unwrap();
285
286 assert_eq!(re_decrypted.name, "Default collection");
287 assert_eq!(re_decrypted.id, expected_id);
288 assert_eq!(re_decrypted.organization_id, expected_org_id);
289 }
290
291 #[tokio::test]
292 async fn test_encrypt_list_decrypt_list_roundtrip() {
293 let collections = test_collections_client().await;
294
295 let views = collections.decrypt_list(vec![test_collection()]).unwrap();
296
297 assert_eq!(views.len(), 1);
298 assert_eq!(views[0].name, "Default collection");
299
300 let expected_id = views[0].id;
301 let expected_org_id = views[0].organization_id;
302
303 let re_encrypted = collections.encrypt_list(views).unwrap();
304
305 assert_eq!(re_encrypted.len(), 1);
306
307 let re_decrypted = collections.decrypt_list(re_encrypted).unwrap();
308
309 assert_eq!(re_decrypted.len(), 1);
310 assert_eq!(re_decrypted[0].name, "Default collection");
311 assert_eq!(re_decrypted[0].id, expected_id);
312 assert_eq!(re_decrypted[0].organization_id, expected_org_id);
313 }
314}