bitwarden_collections/
collection.rs

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/// Type of collection
53#[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    /// Default collection type. Can be assigned by an organization to user(s) or group(s)
63    SharedCollection = 0,
64    /// Default collection assigned to a user for an organization that has
65    /// OrganizationDataOwnership (formerly PersonalOwnership) policy enabled.
66    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    // Helper function to create a test key store with a symmetric key
170    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        // Verify all fields are correctly transferred
265        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}