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, MissingFieldError, OrganizationId, UserId,
7    client::encryption_settings::EncryptionSettingsError, require,
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 = config
39        .api_client
40        .sync_api()
41        .get(input.exclude_subdomains)
42        .await
43        .map_err(|e| SyncError::Api(e.into()))?;
44
45    let org_keys: Vec<_> = require!(sync.profile.as_ref())
46        .organizations
47        .as_deref()
48        .unwrap_or_default()
49        .iter()
50        .filter_map(|o| o.id.zip(o.key.as_deref().and_then(|k| k.parse().ok())))
51        .map(|(id, key)| (OrganizationId::new(id), key))
52        .collect();
53
54    client.internal.initialize_org_crypto(org_keys)?;
55
56    SyncResponse::process_response(sync)
57}
58
59#[derive(Serialize, Deserialize, Debug)]
60#[serde(rename_all = "camelCase", deny_unknown_fields)]
61pub struct ProfileResponse {
62    pub id: UserId,
63    pub name: String,
64    pub email: String,
65
66    //key: String,
67    //private_key: String,
68    pub organizations: Vec<ProfileOrganizationResponse>,
69}
70
71#[derive(Serialize, Deserialize, Debug)]
72#[serde(rename_all = "camelCase", deny_unknown_fields)]
73pub struct ProfileOrganizationResponse {
74    pub id: OrganizationId,
75}
76
77#[derive(Serialize, Deserialize, Debug)]
78#[serde(rename_all = "camelCase", deny_unknown_fields)]
79pub struct DomainResponse {
80    pub equivalent_domains: Vec<Vec<String>>,
81    pub global_equivalent_domains: Vec<GlobalDomains>,
82}
83
84#[allow(missing_docs)]
85#[derive(Serialize, Deserialize, Debug)]
86#[serde(rename_all = "camelCase", deny_unknown_fields)]
87pub struct SyncResponse {
88    /// Data about the user, including their encryption keys and the organizations they are a part
89    /// of
90    pub profile: ProfileResponse,
91    pub folders: Vec<Folder>,
92    pub collections: Vec<Collection>,
93    /// List of ciphers accessible by the user
94    pub ciphers: Vec<Cipher>,
95    pub domains: Option<DomainResponse>,
96    //pub policies: Vec<Policy>,
97    //pub sends: Vec<Send>,
98}
99
100impl SyncResponse {
101    pub(crate) fn process_response(response: SyncResponseModel) -> Result<SyncResponse, SyncError> {
102        let profile = require!(response.profile);
103        let ciphers = require!(response.ciphers);
104
105        fn try_into_iter<In, InItem, Out, OutItem>(iter: In) -> Result<Out, InItem::Error>
106        where
107            In: IntoIterator<Item = InItem>,
108            InItem: TryInto<OutItem>,
109            Out: FromIterator<OutItem>,
110        {
111            iter.into_iter().map(|i| i.try_into()).collect()
112        }
113
114        Ok(SyncResponse {
115            profile: ProfileResponse::process_response(*profile)?,
116            folders: try_into_iter(require!(response.folders))?,
117            collections: try_into_iter(require!(response.collections))?,
118            ciphers: try_into_iter(ciphers)?,
119            domains: response.domains.map(|d| (*d).try_into()).transpose()?,
120            //policies: try_into_iter(require!(response.policies))?,
121            //sends: try_into_iter(require!(response.sends))?,
122        })
123    }
124}
125
126impl ProfileOrganizationResponse {
127    fn process_response(
128        response: ProfileOrganizationResponseModel,
129    ) -> Result<ProfileOrganizationResponse, MissingFieldError> {
130        Ok(ProfileOrganizationResponse {
131            id: OrganizationId::new(require!(response.id)),
132        })
133    }
134}
135
136impl ProfileResponse {
137    fn process_response(
138        response: ProfileResponseModel,
139    ) -> Result<ProfileResponse, MissingFieldError> {
140        Ok(ProfileResponse {
141            id: UserId::new(require!(response.id)),
142            name: require!(response.name),
143            email: require!(response.email),
144            //key: response.key,
145            //private_key: response.private_key,
146            organizations: response
147                .organizations
148                .unwrap_or_default()
149                .into_iter()
150                .map(ProfileOrganizationResponse::process_response)
151                .collect::<Result<_, _>>()?,
152        })
153    }
154}
155
156impl TryFrom<DomainsResponseModel> for DomainResponse {
157    type Error = SyncError;
158
159    fn try_from(value: DomainsResponseModel) -> Result<Self, Self::Error> {
160        Ok(Self {
161            equivalent_domains: value.equivalent_domains.unwrap_or_default(),
162            global_equivalent_domains: value
163                .global_equivalent_domains
164                .unwrap_or_default()
165                .into_iter()
166                .map(|s| s.try_into())
167                .collect::<Result<Vec<GlobalDomains>, _>>()?,
168        })
169    }
170}