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