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