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