Skip to content

Commit

Permalink
Map from Requests to Domain models (#298)
Browse files Browse the repository at this point in the history
Implements many of the translators from Request models to Domain models. Wires us up for hooking-up the sync response handling.
  • Loading branch information
Hinton authored Dec 12, 2023
1 parent 2c85c94 commit f17bac3
Show file tree
Hide file tree
Showing 22 changed files with 545 additions and 45 deletions.
3 changes: 3 additions & 0 deletions crates/bitwarden/src/admin_console/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod policy;

pub use policy::Policy;
70 changes: 70 additions & 0 deletions crates/bitwarden/src/admin_console/policy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
use std::collections::HashMap;

use bitwarden_api_api::models::PolicyResponseModel;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use uuid::Uuid;

use crate::error::{Error, Result};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct Policy {
id: Uuid,
organization_id: Uuid,
r#type: PolicyType,
data: Option<HashMap<String, serde_json::Value>>,
enabled: bool,
}

#[derive(Serialize_repr, Deserialize_repr, Debug, JsonSchema)]
#[repr(u8)]
pub enum PolicyType {
TwoFactorAuthentication = 0, // Requires users to have 2fa enabled
MasterPassword = 1, // Sets minimum requirements for master password complexity
PasswordGenerator = 2, // Sets minimum requirements/default type for generated passwords/passphrases
SingleOrg = 3, // Allows users to only be apart of one organization
RequireSso = 4, // Requires users to authenticate with SSO
PersonalOwnership = 5, // Disables personal vault ownership for adding/cloning items
DisableSend = 6, // Disables the ability to create and edit Bitwarden Sends
SendOptions = 7, // Sets restrictions or defaults for Bitwarden Sends
ResetPassword = 8, // Allows orgs to use reset password : also can enable auto-enrollment during invite flow
MaximumVaultTimeout = 9, // Sets the maximum allowed vault timeout
DisablePersonalVaultExport = 10, // Disable personal vault export
ActivateAutofill = 11, // Activates autofill with page load on the browser extension
}

impl TryFrom<PolicyResponseModel> for Policy {
type Error = Error;

fn try_from(policy: PolicyResponseModel) -> Result<Self> {
Ok(Self {
id: policy.id.ok_or(Error::MissingFields)?,
organization_id: policy.organization_id.ok_or(Error::MissingFields)?,
r#type: policy.r#type.ok_or(Error::MissingFields)?.into(),
data: policy.data,
enabled: policy.enabled.ok_or(Error::MissingFields)?,
})
}
}

impl From<bitwarden_api_api::models::PolicyType> for PolicyType {
fn from(policy_type: bitwarden_api_api::models::PolicyType) -> Self {
match policy_type {
bitwarden_api_api::models::PolicyType::Variant0 => PolicyType::TwoFactorAuthentication,
bitwarden_api_api::models::PolicyType::Variant1 => PolicyType::MasterPassword,
bitwarden_api_api::models::PolicyType::Variant2 => PolicyType::PasswordGenerator,
bitwarden_api_api::models::PolicyType::Variant3 => PolicyType::SingleOrg,
bitwarden_api_api::models::PolicyType::Variant4 => PolicyType::RequireSso,
bitwarden_api_api::models::PolicyType::Variant5 => PolicyType::PersonalOwnership,
bitwarden_api_api::models::PolicyType::Variant6 => PolicyType::DisableSend,
bitwarden_api_api::models::PolicyType::Variant7 => PolicyType::SendOptions,
bitwarden_api_api::models::PolicyType::Variant8 => PolicyType::ResetPassword,
bitwarden_api_api::models::PolicyType::Variant9 => PolicyType::MaximumVaultTimeout,
bitwarden_api_api::models::PolicyType::Variant10 => {
PolicyType::DisablePersonalVaultExport
}
bitwarden_api_api::models::PolicyType::Variant11 => PolicyType::ActivateAutofill,
}
}
}
24 changes: 20 additions & 4 deletions crates/bitwarden/src/crypto/enc_string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ use aes::cipher::{generic_array::GenericArray, typenum::U32};
use base64::Engine;
use serde::{de::Visitor, Deserialize};

use super::{KeyDecryptable, KeyEncryptable, LocateKey};
use crate::{
crypto::{decrypt_aes256_hmac, SymmetricCryptoKey},
error::{CryptoError, EncStringParseError, Error, Result},
util::BASE64_ENGINE,
};

use super::{KeyDecryptable, KeyEncryptable, LocateKey};

/// # Encrypted string primitive
///
/// [EncString] is a Bitwarden specific primitive that represents an encrypted string. They are
Expand Down Expand Up @@ -165,6 +164,12 @@ impl FromStr for EncString {
}

impl EncString {
/// Synthetic sugar for mapping `Option<String>` to `Result<Option<EncString>>`
#[cfg(feature = "internal")]
pub(crate) fn try_from_optional(s: Option<String>) -> Result<Option<EncString>, Error> {
s.map(|s| s.parse()).transpose()
}

#[cfg(feature = "mobile")]
pub(crate) fn from_buffer(buf: &[u8]) -> Result<Self> {
if buf.is_empty() {
Expand Down Expand Up @@ -397,11 +402,22 @@ impl KeyDecryptable<String> for EncString {
}
}

/// Usually we wouldn't want to expose EncStrings in the API or the schemas.
/// But during the transition phase we will expose endpoints using the EncString type.
impl schemars::JsonSchema for crate::crypto::EncString {
fn schema_name() -> String {
"EncString".to_string()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
gen.subschema_for::<String>()
}
}

#[cfg(test)]
mod tests {
use crate::crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey};

use super::EncString;
use crate::crypto::{KeyDecryptable, KeyEncryptable, SymmetricCryptoKey};

#[test]
fn test_enc_string_roundtrip() {
Expand Down
4 changes: 3 additions & 1 deletion crates/bitwarden/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
#[cfg(feature = "mobile")]
uniffi::setup_scaffolding!();

#[cfg(feature = "internal")]
pub mod admin_console;
pub mod auth;
pub mod client;
pub mod crypto;
Expand All @@ -66,7 +68,7 @@ pub mod tool;
#[cfg(feature = "mobile")]
pub(crate) mod uniffi_support;
mod util;
#[cfg(feature = "mobile")]
#[cfg(feature = "internal")]
pub mod vault;
pub mod wordlist;

Expand Down
12 changes: 0 additions & 12 deletions crates/bitwarden/src/mobile/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,3 @@ mod client_kdf;

pub use client_crypto::ClientCrypto;
pub use client_kdf::ClientKdf;

// Usually we wouldn't want to expose EncStrings in the API or the schemas,
// but we need them in the mobile API, so define it here to limit the scope
impl schemars::JsonSchema for crate::crypto::EncString {
fn schema_name() -> String {
"EncString".to_string()
}

fn json_schema(gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
gen.subschema_for::<String>()
}
}
23 changes: 23 additions & 0 deletions crates/bitwarden/src/platform/domain.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::error::{Error, Result};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
pub struct GlobalDomains {
pub r#type: i32,
pub domains: Vec<String>,
pub excluded: bool,
}

impl TryFrom<bitwarden_api_api::models::GlobalDomains> for GlobalDomains {
type Error = Error;

fn try_from(global_domains: bitwarden_api_api::models::GlobalDomains) -> Result<Self> {
Ok(Self {
r#type: global_domains.r#type.ok_or(Error::MissingFields)?,
domains: global_domains.domains.ok_or(Error::MissingFields)?,
excluded: global_domains.excluded.ok_or(Error::MissingFields)?,
})
}
}
1 change: 1 addition & 0 deletions crates/bitwarden/src/platform/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod domain;
mod generate_fingerprint;
mod get_user_api_key;
mod secret_verification_request;
Expand Down
60 changes: 45 additions & 15 deletions crates/bitwarden/src/platform/sync.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
use bitwarden_api_api::models::{
CipherDetailsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel,
SyncResponseModel,
DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use uuid::Uuid;

use super::domain::GlobalDomains;
use crate::{
admin_console::Policy,
client::{encryption_settings::EncryptionSettings, Client},
error::{Error, Result},
vault::{Cipher, Collection, Folder},
};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
Expand Down Expand Up @@ -59,15 +61,23 @@ pub struct ProfileOrganizationResponse {

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct CipherDetailsResponse {}
pub struct DomainResponse {
pub equivalent_domains: Vec<Vec<String>>,
pub global_equivalent_domains: Vec<GlobalDomains>,
}

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct SyncResponse {
/// Data about the user, including their encryption keys and the organizations they are a part of
pub profile: ProfileResponse,
/// List of ciphers accesible by the user
pub ciphers: Vec<CipherDetailsResponse>,
pub folders: Vec<Folder>,
pub collections: Vec<Collection>,
/// List of ciphers accessible by the user
pub ciphers: Vec<Cipher>,
pub domains: Option<DomainResponse>,
pub policies: Vec<Policy>,
pub sends: Vec<crate::vault::Send>,
}

impl SyncResponse {
Expand All @@ -78,22 +88,27 @@ impl SyncResponse {
let profile = *response.profile.ok_or(Error::MissingFields)?;
let ciphers = response.ciphers.ok_or(Error::MissingFields)?;

fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
where
In: IntoIterator<Item = InItem>,
InItem: TryInto<OutItem>,
Out: FromIterator<OutItem>,
{
iter.into_iter().map(|i| i.try_into()).collect()
}

Ok(SyncResponse {
profile: ProfileResponse::process_response(profile, enc)?,
ciphers: ciphers
.into_iter()
.map(CipherDetailsResponse::process_response)
.collect::<Result<_, _>>()?,
folders: try_into_iter(response.folders.ok_or(Error::MissingFields)?)?,
collections: try_into_iter(response.collections.ok_or(Error::MissingFields)?)?,
ciphers: try_into_iter(ciphers)?,
domains: response.domains.map(|d| (*d).try_into()).transpose()?,
policies: try_into_iter(response.policies.ok_or(Error::MissingFields)?)?,
sends: try_into_iter(response.sends.ok_or(Error::MissingFields)?)?,
})
}
}

impl CipherDetailsResponse {
fn process_response(_response: CipherDetailsResponseModel) -> Result<CipherDetailsResponse> {
Ok(CipherDetailsResponse {})
}
}

impl ProfileOrganizationResponse {
fn process_response(
response: ProfileOrganizationResponseModel,
Expand Down Expand Up @@ -124,3 +139,18 @@ impl ProfileResponse {
})
}
}

impl TryFrom<DomainsResponseModel> for DomainResponse {
type Error = Error;
fn try_from(value: DomainsResponseModel) -> Result<Self> {
Ok(Self {
equivalent_domains: value.equivalent_domains.unwrap_or_default(),
global_equivalent_domains: value
.global_equivalent_domains
.unwrap_or_default()
.into_iter()
.map(|s| s.try_into())
.collect::<Result<Vec<GlobalDomains>>>()?,
})
}
}
21 changes: 18 additions & 3 deletions crates/bitwarden/src/vault/cipher/attachment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};

use crate::{
crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey},
error::Result,
error::{Error, Result},
};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
Expand All @@ -28,7 +28,7 @@ pub struct AttachmentView {
pub size: Option<String>,
pub size_name: Option<String>,
pub file_name: Option<String>,
pub key: Option<String>,
pub key: Option<Vec<u8>>, // TODO: Should be made into SymmetricCryptoKey
}

impl KeyEncryptable<Attachment> for AttachmentView {
Expand All @@ -39,7 +39,7 @@ impl KeyEncryptable<Attachment> for AttachmentView {
size: self.size,
size_name: self.size_name,
file_name: self.file_name.encrypt_with_key(key)?,
key: self.key.encrypt_with_key(key)?,
key: self.key.as_deref().encrypt_with_key(key)?,
})
}
}
Expand All @@ -56,3 +56,18 @@ impl KeyDecryptable<AttachmentView> for Attachment {
})
}
}

impl TryFrom<bitwarden_api_api::models::AttachmentResponseModel> for Attachment {
type Error = Error;

fn try_from(attachment: bitwarden_api_api::models::AttachmentResponseModel) -> Result<Self> {
Ok(Self {
id: attachment.id,
url: attachment.url,
size: attachment.size,
size_name: attachment.size_name,
file_name: EncString::try_from_optional(attachment.file_name)?,
key: EncString::try_from_optional(attachment.key)?,
})
}
}
18 changes: 17 additions & 1 deletion crates/bitwarden/src/vault/cipher/card.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
use bitwarden_api_api::models::CipherCardModel;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

use crate::{
crypto::{EncString, KeyDecryptable, KeyEncryptable, SymmetricCryptoKey},
error::Result,
error::{Error, Result},
};

#[derive(Serialize, Deserialize, Debug, JsonSchema)]
Expand Down Expand Up @@ -55,3 +56,18 @@ impl KeyDecryptable<CardView> for Card {
})
}
}

impl TryFrom<CipherCardModel> for Card {
type Error = Error;

fn try_from(card: CipherCardModel) -> Result<Self> {
Ok(Self {
cardholder_name: EncString::try_from_optional(card.cardholder_name)?,
exp_month: EncString::try_from_optional(card.exp_month)?,
exp_year: EncString::try_from_optional(card.exp_year)?,
code: EncString::try_from_optional(card.code)?,
brand: EncString::try_from_optional(card.brand)?,
number: EncString::try_from_optional(card.number)?,
})
}
}
Loading

0 comments on commit f17bac3

Please sign in to comment.