1use bitwarden_api_api::models::CollectionDetailsResponseModel;
2use bitwarden_core::{
3 OrganizationId,
4 key_management::{KeyIds, SymmetricKeyId},
5 require,
6};
7use bitwarden_crypto::{CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext};
8use bitwarden_uuid::uuid_newtype;
9use serde::{Deserialize, Serialize};
10use serde_repr::{Deserialize_repr, Serialize_repr};
11use uuid::Uuid;
12#[cfg(feature = "wasm")]
13use {tsify::Tsify, wasm_bindgen::prelude::*};
14
15use crate::{error::CollectionsParseError, tree::TreeItem};
16
17uuid_newtype!(pub CollectionId);
18
19#[allow(missing_docs)]
20#[derive(Serialize, Deserialize, Debug)]
21#[serde(rename_all = "camelCase", deny_unknown_fields)]
22#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
23#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
24pub struct Collection {
25 pub id: Option<CollectionId>,
26 pub organization_id: OrganizationId,
27 pub name: EncString,
28 pub external_id: Option<String>,
29 pub hide_passwords: bool,
30 pub read_only: bool,
31 pub manage: bool,
32 pub default_user_collection_email: Option<String>,
33 pub r#type: CollectionType,
34}
35
36#[allow(missing_docs)]
37#[derive(Serialize, Deserialize, Debug, Clone)]
38#[serde(rename_all = "camelCase", deny_unknown_fields)]
39#[cfg_attr(feature = "uniffi", derive(uniffi::Record))]
40#[cfg_attr(feature = "wasm", derive(Tsify), tsify(into_wasm_abi, from_wasm_abi))]
41pub struct CollectionView {
42 pub id: Option<CollectionId>,
43 pub organization_id: OrganizationId,
44 pub name: String,
45 pub external_id: Option<String>,
46 pub hide_passwords: bool,
47 pub read_only: bool,
48 pub manage: bool,
49 pub r#type: CollectionType,
50}
51
52#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq)]
54#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
55#[cfg_attr(
56 feature = "wasm",
57 derive(tsify::Tsify),
58 tsify(into_wasm_abi, from_wasm_abi)
59)]
60#[repr(u8)]
61pub enum CollectionType {
62 SharedCollection = 0,
64 DefaultUserCollection = 1,
67}
68
69#[allow(missing_docs)]
70impl Decryptable<KeyIds, SymmetricKeyId, CollectionView> for Collection {
71 fn decrypt(
72 &self,
73 ctx: &mut KeyStoreContext<KeyIds>,
74 key: SymmetricKeyId,
75 ) -> Result<CollectionView, CryptoError> {
76 let name = self
77 .default_user_collection_email
78 .as_ref()
79 .unwrap_or(&self.name.decrypt(ctx, key)?)
80 .clone();
81
82 Ok(CollectionView {
83 id: self.id,
84 organization_id: self.organization_id,
85 name,
86 external_id: self.external_id.clone(),
87 hide_passwords: self.hide_passwords,
88 read_only: self.read_only,
89 manage: self.manage,
90 r#type: self.r#type.clone(),
91 })
92 }
93}
94
95#[allow(missing_docs)]
96impl TryFrom<CollectionDetailsResponseModel> for Collection {
97 type Error = CollectionsParseError;
98
99 fn try_from(collection: CollectionDetailsResponseModel) -> Result<Self, Self::Error> {
100 Ok(Collection {
101 id: collection.id.map(CollectionId::new),
102 organization_id: OrganizationId::new(require!(collection.organization_id)),
103 name: require!(collection.name).parse()?,
104 external_id: collection.external_id,
105 hide_passwords: collection.hide_passwords.unwrap_or(false),
106 read_only: collection.read_only.unwrap_or(false),
107 manage: collection.manage.unwrap_or(false),
108 default_user_collection_email: collection.default_user_collection_email,
109 r#type: require!(collection.r#type).try_into()?,
110 })
111 }
112}
113
114#[allow(missing_docs)]
115impl IdentifyKey<SymmetricKeyId> for Collection {
116 fn key_identifier(&self) -> SymmetricKeyId {
117 SymmetricKeyId::Organization(self.organization_id)
118 }
119}
120
121#[allow(missing_docs)]
122impl TreeItem for CollectionView {
123 fn id(&self) -> Uuid {
124 self.id.map(|id| id.0).unwrap_or_default()
125 }
126
127 fn short_name(&self) -> &str {
128 self.path().last().unwrap_or(&"")
129 }
130
131 fn path(&self) -> Vec<&str> {
132 self.name
133 .split(Self::DELIMITER)
134 .filter(|s| !s.is_empty())
135 .collect::<Vec<&str>>()
136 }
137
138 const DELIMITER: char = '/';
139}
140
141impl TryFrom<bitwarden_api_api::models::CollectionType> for CollectionType {
142 type Error = bitwarden_core::MissingFieldError;
143
144 fn try_from(
145 collection_type: bitwarden_api_api::models::CollectionType,
146 ) -> Result<Self, Self::Error> {
147 Ok(match collection_type {
148 bitwarden_api_api::models::CollectionType::SharedCollection => Self::SharedCollection,
149 bitwarden_api_api::models::CollectionType::DefaultUserCollection => {
150 Self::DefaultUserCollection
151 }
152 bitwarden_api_api::models::CollectionType::__Unknown(_) => {
153 return Err(bitwarden_core::MissingFieldError("type"));
154 }
155 })
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
162 use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricKeyAlgorithm};
163
164 use super::*;
165
166 const ORGANIZATION_ID: &str = "12345678-1234-1234-1234-123456789012";
167 const COLLECTION_ID: &str = "87654321-4321-4321-4321-210987654321";
168
169 fn create_test_key_store() -> KeyStore<KeyIds> {
171 let store = KeyStore::<KeyIds>::default();
172 let org_id = ORGANIZATION_ID.parse().unwrap();
173
174 let mut ctx = store.context_mut();
175
176 let local_key_id = ctx.make_symmetric_key(SymmetricKeyAlgorithm::Aes256CbcHmac);
177 ctx.persist_symmetric_key(local_key_id, SymmetricKeyId::Organization(org_id))
178 .unwrap();
179 drop(ctx);
180
181 store
182 }
183
184 #[test]
185 fn test_decrypt_with_name_only() {
186 let store = create_test_key_store();
187 let mut ctx = store.context();
188 let org_id = ORGANIZATION_ID.parse().unwrap();
189 let key = SymmetricKeyId::Organization(org_id);
190
191 let collection_name: &str = "Collection Name";
192
193 let collection = Collection {
194 id: Some(COLLECTION_ID.parse().unwrap()),
195 organization_id: org_id,
196 name: collection_name.encrypt(&mut ctx, key).unwrap(),
197 external_id: Some("external-id".to_string()),
198 hide_passwords: true,
199 read_only: false,
200 manage: true,
201 default_user_collection_email: None,
202 r#type: CollectionType::SharedCollection,
203 };
204
205 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
206
207 assert_eq!(decrypted.name, collection_name);
208 }
209
210 #[test]
211 fn test_decrypt_with_default_user_collection_email() {
212 let store = create_test_key_store();
213 let mut ctx = store.context();
214 let org_id = ORGANIZATION_ID.parse().unwrap();
215 let key = SymmetricKeyId::Organization(org_id);
216
217 let collection_name: &str = "Collection Name";
218 let default_user_collection_email = String::from("[email protected]");
219
220 let collection = Collection {
221 id: Some(COLLECTION_ID.parse().unwrap()),
222 organization_id: org_id,
223 name: collection_name.encrypt(&mut ctx, key).unwrap(),
224 external_id: None,
225 hide_passwords: false,
226 read_only: true,
227 manage: false,
228 default_user_collection_email: Some(default_user_collection_email.clone()),
229 r#type: CollectionType::SharedCollection,
230 };
231
232 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
233
234 assert_ne!(decrypted.name, collection_name);
235 assert_eq!(decrypted.name, default_user_collection_email);
236 }
237
238 #[test]
239 fn test_decrypt_all_fields_preserved() {
240 let store = create_test_key_store();
241 let mut ctx = store.context();
242 let org_id = ORGANIZATION_ID.parse().unwrap();
243 let key = SymmetricKeyId::Organization(org_id);
244
245 let collection_id = Some(COLLECTION_ID.parse().unwrap());
246 let external_id = Some("external-test-id".to_string());
247 let collection_name: &str = "Collection Name";
248 let collection_type = CollectionType::SharedCollection;
249
250 let collection = Collection {
251 id: collection_id,
252 organization_id: org_id,
253 name: collection_name.encrypt(&mut ctx, key).unwrap(),
254 external_id: external_id.clone(),
255 hide_passwords: true,
256 read_only: true,
257 manage: true,
258 default_user_collection_email: None,
259 r#type: collection_type.clone(),
260 };
261
262 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
263
264 assert_eq!(decrypted.id, collection.id);
266 assert_eq!(decrypted.organization_id, collection.organization_id);
267 assert_eq!(decrypted.name, collection_name);
268 assert_eq!(decrypted.external_id, external_id);
269 assert_eq!(decrypted.hide_passwords, collection.hide_passwords);
270 assert_eq!(decrypted.read_only, collection.read_only);
271 assert_eq!(decrypted.manage, collection.manage);
272 assert_eq!(decrypted.r#type, collection_type);
273 }
274}