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).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    // Helper function to create a test key store with a symmetric key
163    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        // Verify all fields are correctly transferred
258        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}