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).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 From<bitwarden_api_api::models::CollectionType> for CollectionType {
142 fn from(collection_type: bitwarden_api_api::models::CollectionType) -> Self {
143 match collection_type {
144 bitwarden_api_api::models::CollectionType::SharedCollection => Self::SharedCollection,
145 bitwarden_api_api::models::CollectionType::DefaultUserCollection => {
146 Self::DefaultUserCollection
147 }
148 }
149 }
150}
151
152#[cfg(test)]
153mod tests {
154 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
155 use bitwarden_crypto::{KeyStore, PrimitiveEncryptable, SymmetricCryptoKey};
156
157 use super::*;
158
159 const ORGANIZATION_ID: &str = "12345678-1234-1234-1234-123456789012";
160 const COLLECTION_ID: &str = "87654321-4321-4321-4321-210987654321";
161
162 fn create_test_key_store() -> KeyStore<KeyIds> {
164 let store = KeyStore::<KeyIds>::default();
165 let key = SymmetricCryptoKey::make_aes256_cbc_hmac_key();
166 let org_id = ORGANIZATION_ID.parse().unwrap();
167
168 #[allow(deprecated)]
169 store
170 .context_mut()
171 .set_symmetric_key(SymmetricKeyId::Organization(org_id), key)
172 .unwrap();
173
174 store
175 }
176
177 #[test]
178 fn test_decrypt_with_name_only() {
179 let store = create_test_key_store();
180 let mut ctx = store.context();
181 let org_id = ORGANIZATION_ID.parse().unwrap();
182 let key = SymmetricKeyId::Organization(org_id);
183
184 let collection_name: &str = "Collection Name";
185
186 let collection = Collection {
187 id: Some(COLLECTION_ID.parse().unwrap()),
188 organization_id: org_id,
189 name: collection_name.encrypt(&mut ctx, key).unwrap(),
190 external_id: Some("external-id".to_string()),
191 hide_passwords: true,
192 read_only: false,
193 manage: true,
194 default_user_collection_email: None,
195 r#type: CollectionType::SharedCollection,
196 };
197
198 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
199
200 assert_eq!(decrypted.name, collection_name);
201 }
202
203 #[test]
204 fn test_decrypt_with_default_user_collection_email() {
205 let store = create_test_key_store();
206 let mut ctx = store.context();
207 let org_id = ORGANIZATION_ID.parse().unwrap();
208 let key = SymmetricKeyId::Organization(org_id);
209
210 let collection_name: &str = "Collection Name";
211 let default_user_collection_email = String::from("[email protected]");
212
213 let collection = Collection {
214 id: Some(COLLECTION_ID.parse().unwrap()),
215 organization_id: org_id,
216 name: collection_name.encrypt(&mut ctx, key).unwrap(),
217 external_id: None,
218 hide_passwords: false,
219 read_only: true,
220 manage: false,
221 default_user_collection_email: Some(default_user_collection_email.clone()),
222 r#type: CollectionType::SharedCollection,
223 };
224
225 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
226
227 assert_ne!(decrypted.name, collection_name);
228 assert_eq!(decrypted.name, default_user_collection_email);
229 }
230
231 #[test]
232 fn test_decrypt_all_fields_preserved() {
233 let store = create_test_key_store();
234 let mut ctx = store.context();
235 let org_id = ORGANIZATION_ID.parse().unwrap();
236 let key = SymmetricKeyId::Organization(org_id);
237
238 let collection_id = Some(COLLECTION_ID.parse().unwrap());
239 let external_id = Some("external-test-id".to_string());
240 let collection_name: &str = "Collection Name";
241 let collection_type = CollectionType::SharedCollection;
242
243 let collection = Collection {
244 id: collection_id,
245 organization_id: org_id,
246 name: collection_name.encrypt(&mut ctx, key).unwrap(),
247 external_id: external_id.clone(),
248 hide_passwords: true,
249 read_only: true,
250 manage: true,
251 default_user_collection_email: None,
252 r#type: collection_type.clone(),
253 };
254
255 let decrypted = collection.decrypt(&mut ctx, key).unwrap();
256
257 assert_eq!(decrypted.id, collection.id);
259 assert_eq!(decrypted.organization_id, collection.organization_id);
260 assert_eq!(decrypted.name, collection_name);
261 assert_eq!(decrypted.external_id, external_id);
262 assert_eq!(decrypted.hide_passwords, collection.hide_passwords);
263 assert_eq!(decrypted.read_only, collection.read_only);
264 assert_eq!(decrypted.manage, collection.manage);
265 assert_eq!(decrypted.r#type, collection_type);
266 }
267}