bitwarden_vault/
sync.rs

1use bitwarden_api_api::models::{
2    DomainsResponseModel, ProfileOrganizationResponseModel, ProfileResponseModel, SyncResponseModel,
3};
4use bitwarden_collections::{collection::Collection, error::CollectionsParseError};
5use bitwarden_core::{
6    client::encryption_settings::EncryptionSettingsError, require, Client, MissingFieldError,
7    OrganizationId, UserId,
8};
9use serde::{Deserialize, Serialize};
10use thiserror::Error;
11
12use crate::{Cipher, Folder, GlobalDomains, VaultParseError};
13
14#[derive(Debug, Error)]
15pub enum SyncError {
16    #[error(transparent)]
17    Api(#[from] bitwarden_core::ApiError),
18    #[error(transparent)]
19    MissingField(#[from] MissingFieldError),
20    #[error(transparent)]
21    VaultParse(#[from] VaultParseError),
22    #[error(transparent)]
23    CollectionParse(#[from] CollectionsParseError),
24    #[error(transparent)]
25    EncryptionSettings(#[from] EncryptionSettingsError),
26}
27
28#[allow(missing_docs)]
29#[derive(Serialize, Deserialize, Debug)]
30#[serde(rename_all = "camelCase", deny_unknown_fields)]
31pub struct SyncRequest {
32    /// Exclude the subdomains from the response, defaults to false
33    pub exclude_subdomains: Option<bool>,
34}
35
36pub(crate) async fn sync(client: &Client, input: &SyncRequest) -> Result<SyncResponse, SyncError> {
37    let config = client.internal.get_api_configurations().await;
38    let sync = bitwarden_api_api::apis::sync_api::sync_get(&config.api, input.exclude_subdomains)
39        .await
40        .map_err(|e| SyncError::Api(e.into()))?;
41
42    let org_keys: Vec<_> = require!(sync.profile.as_ref())
43        .organizations
44        .as_deref()
45        .unwrap_or_default()
46        .iter()
47        .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
48        .map(|(id, key)| (OrganizationId::new(id), key))
49        .collect();
50
51    client.internal.initialize_org_crypto(org_keys)?;
52
53    SyncResponse::process_response(sync)
54}
55
56#[derive(Serialize, Deserialize, Debug)]
57#[serde(rename_all = "camelCase", deny_unknown_fields)]
58pub struct ProfileResponse {
59    pub id: UserId,
60    pub name: String,
61    pub email: String,
62
63    //key: String,
64    //private_key: String,
65    pub organizations: Vec<ProfileOrganizationResponse>,
66}
67
68#[derive(Serialize, Deserialize, Debug)]
69#[serde(rename_all = "camelCase", deny_unknown_fields)]
70pub struct ProfileOrganizationResponse {
71    pub id: OrganizationId,
72}
73
74#[derive(Serialize, Deserialize, Debug)]
75#[serde(rename_all = "camelCase", deny_unknown_fields)]
76pub struct DomainResponse {
77    pub equivalent_domains: Vec<Vec<String>>,
78    pub global_equivalent_domains: Vec<GlobalDomains>,
79}
80
81#[allow(missing_docs)]
82#[derive(Serialize, Deserialize, Debug)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct SyncResponse {
85    /// Data about the user, including their encryption keys and the organizations they are a part
86    /// of
87    pub profile: ProfileResponse,
88    pub folders: Vec<Folder>,
89    pub collections: Vec<Collection>,
90    /// List of ciphers accessible by the user
91    pub ciphers: Vec<Cipher>,
92    pub domains: Option<DomainResponse>,
93    //pub policies: Vec<Policy>,
94    //pub sends: Vec<Send>,
95}
96
97impl SyncResponse {
98    pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
99        let profile = require!(response.profile);
100        let ciphers = require!(response.ciphers);
101
102        fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
103        where
104            In: IntoIterator<Item = InItem>,
105            InItem: TryInto<OutItem>,
106            Out: FromIterator<OutItem>,
107        {
108            iter.into_iter().map(|i| i.try_into()).collect()
109        }
110
111        Ok(SyncResponse {
112            profile: ProfileResponse::process_response(*profile)?,
113            folders: try_into_iter(require!(response.folders))?,
114            collections: try_into_iter(require!(response.collections))?,
115            ciphers: try_into_iter(ciphers)?,
116            domains: response.domains.map(|d| (*d).try_into()).transpose()?,
117            //policies: try_into_iter(require!(response.policies))?,
118            //sends: try_into_iter(require!(response.sends))?,
119        })
120    }
121}
122
123impl ProfileOrganizationResponse {
124    fn process_response(
125        response: ProfileOrganizationResponseModel,
126    ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
127        Ok(ProfileOrganizationResponse {
128            id: OrganizationId::new(require!(response.id)),
129        })
130    }
131}
132
133impl ProfileResponse {
134    fn process_response(
135        response: ProfileResponseModel,
136    ) -> Result<ProfileResponse, MissingFieldError> {
137        Ok(ProfileResponse {
138            id: UserId::new(require!(response.id)),
139            name: require!(response.name),
140            email: require!(response.email),
141            //key: response.key,
142            //private_key: response.private_key,
143            organizations: response
144                .organizations
145                .unwrap_or_default()
146                .into_iter()
147                .map(ProfileOrganizationResponse::process_response)
148                .collect::<Result<_, _>>()?,
149        })
150    }
151}
152
153impl TryFrom<DomainsResponseModel> for DomainResponse {
154    type Error = SyncError;
155
156    fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
157        Ok(Self {
158            equivalent_domains: value.equivalent_domains.unwrap_or_default(),
159            global_equivalent_domains: value
160                .global_equivalent_domains
161                .unwrap_or_default()
162                .into_iter()
163                .map(|s| s.try_into())
164                .collect::<Result<Vec<GlobalDomains>, _>>()?,
165        })
166    }
167}