Skip to content

Commit bceb681

Browse files
authored
[PM-22256] Add type and default_user_collection_email to collection (#371)
## 🎟️ Tracking [PM-22256](https://bitwarden.atlassian.net/browse/PM-22256) ## 📔 Objective This adds type and default user collection email to the Collection struct. Type is only added to CollectionView. Name is now computed by the decrypt function of Collection as it is unneeded on the CollectionView. [PM-22256]: https://bitwarden.atlassian.net/browse/PM-22256?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ
1 parent 4813492 commit bceb681

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/bitwarden-collections/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ bitwarden-core = { workspace = true, features = ["internal"] }
2828
bitwarden-crypto = { workspace = true }
2929
bitwarden-error = { workspace = true }
3030
serde = { workspace = true }
31+
serde_repr = { workspace = true }
3132
thiserror = { workspace = true }
3233
tsify = { workspace = true, optional = true }
3334
uniffi = { workspace = true, optional = true }

crates/bitwarden-collections/src/collection.rs

Lines changed: 160 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use bitwarden_core::{
55
};
66
use bitwarden_crypto::{CryptoError, Decryptable, EncString, IdentifyKey, KeyStoreContext};
77
use serde::{Deserialize, Serialize};
8+
use serde_repr::{Deserialize_repr, Serialize_repr};
89
use uuid::Uuid;
910
#[cfg(feature = "wasm")]
1011
use {tsify::Tsify, wasm_bindgen::prelude::*};
@@ -24,6 +25,8 @@ pub struct Collection {
2425
pub hide_passwords: bool,
2526
pub read_only: bool,
2627
pub manage: bool,
28+
pub default_user_collection_email: Option<String>,
29+
pub r#type: CollectionType,
2730
}
2831

2932
#[allow(missing_docs)]
@@ -39,6 +42,24 @@ pub struct CollectionView {
3942
pub hide_passwords: bool,
4043
pub read_only: bool,
4144
pub manage: bool,
45+
pub r#type: CollectionType,
46+
}
47+
48+
/// Type of collection
49+
#[derive(Serialize_repr, Deserialize_repr, Debug, Clone, Eq, PartialEq)]
50+
#[cfg_attr(feature = "uniffi", derive(uniffi::Enum))]
51+
#[cfg_attr(
52+
feature = "wasm",
53+
derive(tsify::Tsify),
54+
tsify(into_wasm_abi, from_wasm_abi)
55+
)]
56+
#[repr(u8)]
57+
pub enum CollectionType {
58+
/// Default collection type. Can be assigned by an organization to user(s) or group(s)
59+
SharedCollection = 0,
60+
/// Default collection assigned to a user for an organization that has
61+
/// OrganizationDataOwnership (formerly PersonalOwnership) policy enabled.
62+
DefaultUserCollection = 1,
4263
}
4364

4465
#[allow(missing_docs)]
@@ -48,14 +69,21 @@ impl Decryptable<KeyIds, SymmetricKeyId, CollectionView> for Collection {
4869
ctx: &mut KeyStoreContext<KeyIds>,
4970
key: SymmetricKeyId,
5071
) -> Result<CollectionView, CryptoError> {
72+
let name = self
73+
.default_user_collection_email
74+
.as_ref()
75+
.unwrap_or(&self.name.decrypt(ctx, key)?)
76+
.clone();
77+
5178
Ok(CollectionView {
5279
id: self.id,
5380
organization_id: self.organization_id,
54-
name: self.name.decrypt(ctx, key).ok().unwrap_or_default(),
81+
name,
5582
external_id: self.external_id.clone(),
5683
hide_passwords: self.hide_passwords,
5784
read_only: self.read_only,
5885
manage: self.manage,
86+
r#type: self.r#type.clone(),
5987
})
6088
}
6189
}
@@ -73,6 +101,8 @@ impl TryFrom<CollectionDetailsResponseModel> for Collection {
73101
hide_passwords: collection.hide_passwords.unwrap_or(false),
74102
read_only: collection.read_only.unwrap_or(false),
75103
manage: collection.manage.unwrap_or(false),
104+
default_user_collection_email: collection.default_user_collection_email,
105+
r#type: require!(collection.r#type).into(),
76106
})
77107
}
78108
}
@@ -103,3 +133,132 @@ impl TreeItem for CollectionView {
103133

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

crates/bitwarden-vault/src/collection_client.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ impl CollectionViewTree {
110110

111111
#[cfg(test)]
112112
mod tests {
113+
use bitwarden_collections::collection::CollectionType;
113114
use bitwarden_core::client::test_accounts::test_bitwarden_com_account;
114115

115116
use super::*;
@@ -127,6 +128,8 @@ mod tests {
127128
hide_passwords: false,
128129
read_only: false,
129130
manage: false,
131+
default_user_collection_email: None,
132+
r#type: CollectionType::SharedCollection,
130133
}]).unwrap();
131134

132135
assert_eq!(dec[0].name, "Default collection");
@@ -144,6 +147,8 @@ mod tests {
144147
hide_passwords: false,
145148
read_only: false,
146149
manage: false,
150+
default_user_collection_email: None,
151+
r#type: CollectionType::SharedCollection,
147152
}).unwrap();
148153

149154
assert_eq!(dec.name, "Default collection");

0 commit comments

Comments
 (0)