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 }
201 }
202
203 async fn test_collections_client() -> CollectionsClient {
204 let client = Client::init_test_account(test_bitwarden_com_account()).await;
205 CollectionsClient::from_client(&client)
206 }
207
208 #[tokio::test]
209 async fn test_decrypt_list() {
210 let collections = test_collections_client().await;
211
212 let dec = collections.decrypt_list(vec![test_collection()]).unwrap();
213
214 assert_eq!(dec[0].name, "Default collection");
215 }
216
217 #[tokio::test]
218 async fn test_decrypt() {
219 let collections = test_collections_client().await;
220
221 let dec = collections.decrypt(test_collection()).unwrap();
222
223 assert_eq!(dec.name, "Default collection");
224 }
225
226 #[tokio::test]
227 async fn test_decrypt_list_with_failures_all_success() {
228 let collections = test_collections_client().await;
229
230 let result = collections.decrypt_list_with_failures(vec![test_collection()]);
231
232 assert_eq!(result.successes.len(), 1);
233 assert!(result.failures.is_empty());
234 assert_eq!(result.successes[0].name, "Default collection");
235 }
236
237 #[tokio::test]
238 async fn test_decrypt_list_with_failures_mixed_results() {
239 let client = test_collections_client().await;
240
241 let valid_collection = test_collection();
242 let mut invalid_collection = test_collection();
243 invalid_collection.organization_id = OrganizationId::new_v4();
246
247 let collections = vec![valid_collection, invalid_collection.clone()];
248
249 let result = client.decrypt_list_with_failures(collections);
250
251 assert_eq!(result.successes.len(), 1);
252 assert_eq!(result.successes[0].name, "Default collection");
253
254 assert_eq!(result.failures.len(), 1);
255 assert_eq!(result.failures[0].id, invalid_collection.id);
258 assert_eq!(result.failures[0].name, invalid_collection.name);
259 }
260
261 #[tokio::test]
262 async fn test_decrypt_list_with_failures_empty_list() {
263 let collections = test_collections_client().await;
264
265 let result = collections.decrypt_list_with_failures(vec![]);
266
267 assert!(result.successes.is_empty());
268 assert!(result.failures.is_empty());
269 }
270
271 #[tokio::test]
272 async fn test_encrypt_decrypt_roundtrip() {
273 let collections = test_collections_client().await;
274
275 let view = collections.decrypt(test_collection()).unwrap();
276
277 assert_eq!(view.name, "Default collection");
278
279 let expected_id = view.id;
281 let expected_org_id = view.organization_id;
282 let re_encrypted = collections.encrypt(view).unwrap();
283 let re_decrypted = collections.decrypt(re_encrypted).unwrap();
284
285 assert_eq!(re_decrypted.name, "Default collection");
286 assert_eq!(re_decrypted.id, expected_id);
287 assert_eq!(re_decrypted.organization_id, expected_org_id);
288 }
289
290 #[tokio::test]
291 async fn test_encrypt_list_decrypt_list_roundtrip() {
292 let collections = test_collections_client().await;
293
294 let views = collections.decrypt_list(vec![test_collection()]).unwrap();
295
296 assert_eq!(views.len(), 1);
297 assert_eq!(views[0].name, "Default collection");
298
299 let expected_id = views[0].id;
300 let expected_org_id = views[0].organization_id;
301
302 let re_encrypted = collections.encrypt_list(views).unwrap();
303
304 assert_eq!(re_encrypted.len(), 1);
305
306 let re_decrypted = collections.decrypt_list(re_encrypted).unwrap();
307
308 assert_eq!(re_decrypted.len(), 1);
309 assert_eq!(re_decrypted[0].name, "Default collection");
310 assert_eq!(re_decrypted[0].id, expected_id);
311 assert_eq!(re_decrypted[0].organization_id, expected_org_id);
312 }
313}