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