bitwarden_vault/cipher/cipher_client/admin/
restore.rs

1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeyIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10    Cipher, CipherId, CipherView, DecryptCipherListResult, VaultParseError,
11    cipher::cipher::PartialCipher, cipher_client::admin::CipherAdminClient,
12};
13
14#[allow(missing_docs)]
15#[bitwarden_error(flat)]
16#[derive(Debug, Error)]
17pub enum RestoreCipherAdminError {
18    #[error(transparent)]
19    Api(#[from] ApiError),
20    #[error(transparent)]
21    VaultParse(#[from] VaultParseError),
22    #[error(transparent)]
23    Crypto(#[from] CryptoError),
24}
25
26impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherAdminError {
27    fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
28        Self::Api(val.into())
29    }
30}
31
32/// Restores a soft-deleted cipher on the server, using the admin endpoint.
33pub async fn restore_as_admin(
34    cipher_id: CipherId,
35    api_client: &ApiClient,
36    key_store: &KeyStore<KeyIds>,
37) -> Result<CipherView, RestoreCipherAdminError> {
38    let api = api_client.ciphers_api();
39
40    let cipher: Cipher = api
41        .put_restore_admin(cipher_id.into())
42        .await?
43        .merge_with_cipher(None)?;
44
45    Ok(key_store.decrypt(&cipher)?)
46}
47
48/// Restores multiple soft-deleted ciphers on the server.
49pub async fn restore_many_as_admin(
50    cipher_ids: Vec<CipherId>,
51    org_id: OrganizationId,
52    api_client: &ApiClient,
53    key_store: &KeyStore<KeyIds>,
54) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
55    let api = api_client.ciphers_api();
56
57    let ciphers: Vec<Cipher> = api
58        .put_restore_many_admin(Some(CipherBulkRestoreRequestModel {
59            ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
60            organization_id: Some(org_id.into()),
61        }))
62        .await?
63        .data
64        .into_iter()
65        .flatten()
66        .map(|c| c.merge_with_cipher(None))
67        .collect::<Result<Vec<_>, _>>()?;
68
69    let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
70    Ok(DecryptCipherListResult {
71        successes,
72        failures: failures.into_iter().cloned().collect(),
73    })
74}
75
76#[cfg_attr(feature = "wasm", wasm_bindgen)]
77impl CipherAdminClient {
78    /// Restores a soft-deleted cipher on the server, using the admin endpoint.
79    pub async fn restore(
80        &self,
81        cipher_id: CipherId,
82    ) -> Result<CipherView, RestoreCipherAdminError> {
83        let api_client = &self
84            .client
85            .internal
86            .get_api_configurations()
87            .await
88            .api_client;
89        let key_store = self.client.internal.get_key_store();
90
91        restore_as_admin(cipher_id, api_client, key_store).await
92    }
93    /// Restores multiple soft-deleted ciphers on the server.
94    pub async fn restore_many(
95        &self,
96        cipher_ids: Vec<CipherId>,
97        org_id: OrganizationId,
98    ) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
99        let api_client = &self
100            .client
101            .internal
102            .get_api_configurations()
103            .await
104            .api_client;
105        let key_store = self.client.internal.get_key_store();
106
107        restore_many_as_admin(cipher_ids, org_id, api_client, key_store).await
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use bitwarden_api_api::{
114        apis::ApiClient,
115        models::{CipherMiniResponseModel, CipherMiniResponseModelListResponseModel},
116    };
117    use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
118    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
119    use chrono::Utc;
120
121    use super::*;
122    use crate::{Cipher, CipherId, Login};
123
124    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
125    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
126    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
127
128    fn generate_test_cipher() -> Cipher {
129        Cipher {
130            id: TEST_CIPHER_ID.parse().ok(),
131            name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
132            r#type: crate::CipherType::Login,
133            notes: Default::default(),
134            organization_id: Default::default(),
135            folder_id: Default::default(),
136            favorite: Default::default(),
137            reprompt: Default::default(),
138            fields: Default::default(),
139            collection_ids: Default::default(),
140            key: Default::default(),
141            login: Some(Login{
142                username: None,
143                password: None,
144                password_revision_date: None,
145                uris: None, totp: None,
146                autofill_on_page_load: None,
147                fido2_credentials: None,
148            }),
149            identity: Default::default(),
150            card: Default::default(),
151            secure_note: Default::default(),
152            ssh_key: Default::default(),
153            organization_use_totp: Default::default(),
154            edit: Default::default(),
155            permissions: Default::default(),
156            view_password: Default::default(),
157            local_data: Default::default(),
158            attachments: Default::default(),
159            password_history: Default::default(),
160            creation_date: Default::default(),
161            deleted_date: Default::default(),
162            revision_date: Default::default(),
163            archived_date: Default::default(),
164            data: Default::default(),
165        }
166    }
167
168    #[tokio::test]
169    async fn test_restore_as_admin() {
170        let mut cipher = generate_test_cipher();
171        cipher.deleted_date = Some(Utc::now());
172
173        let api_client = {
174            let cipher = cipher.clone();
175            ApiClient::new_mocked(move |mock| {
176                mock.ciphers_api
177                    .expect_put_restore_admin()
178                    .returning(move |_model| {
179                        Ok(CipherMiniResponseModel {
180                            id: Some(TEST_CIPHER_ID.try_into().unwrap()),
181                            name: Some(cipher.name.to_string()),
182                            r#type: Some(cipher.r#type.into()),
183                            creation_date: Some(cipher.creation_date.to_string()),
184                            revision_date: Some(Utc::now().to_rfc3339()),
185                            login: cipher.login.clone().map(|l| Box::new(l.into())),
186                            ..Default::default()
187                        })
188                    });
189            })
190        };
191
192        let store: KeyStore<KeyIds> = KeyStore::default();
193        #[allow(deprecated)]
194        let _ = store.context_mut().set_symmetric_key(
195            SymmetricKeyId::User,
196            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
197        );
198        let start_time = Utc::now();
199        let updated_cipher = restore_as_admin(TEST_CIPHER_ID.parse().unwrap(), &api_client, &store)
200            .await
201            .unwrap();
202        let end_time = Utc::now();
203
204        assert!(updated_cipher.deleted_date.is_none());
205        assert!(
206            updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
207        );
208    }
209
210    #[tokio::test]
211    async fn test_restore_many_as_admin() {
212        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
213        let mut cipher_1 = generate_test_cipher();
214        cipher_1.deleted_date = Some(Utc::now());
215        let mut cipher_2 = generate_test_cipher();
216        cipher_2.deleted_date = Some(Utc::now());
217        cipher_2.id = Some(cipher_id_2);
218
219        let api_client = ApiClient::new_mocked(move |mock| {
220            mock.ciphers_api
221                .expect_put_restore_many_admin()
222                .returning(move |_model| {
223                    Ok(CipherMiniResponseModelListResponseModel {
224                        object: None,
225                        data: Some(vec![
226                            CipherMiniResponseModel {
227                                id: cipher_1.id.map(|id| id.into()),
228                                name: Some(cipher_1.name.to_string()),
229                                r#type: Some(cipher_1.r#type.into()),
230                                login: cipher_1.login.clone().map(|l| Box::new(l.into())),
231                                creation_date: cipher_1.creation_date.to_string().into(),
232                                deleted_date: None,
233                                revision_date: Some(Utc::now().to_rfc3339()),
234                                ..Default::default()
235                            },
236                            CipherMiniResponseModel {
237                                id: cipher_2.id.map(|id| id.into()),
238                                name: Some(cipher_2.name.to_string()),
239                                r#type: Some(cipher_2.r#type.into()),
240                                login: cipher_2.login.clone().map(|l| Box::new(l.into())),
241                                creation_date: cipher_2.creation_date.to_string().into(),
242                                deleted_date: None,
243                                revision_date: Some(Utc::now().to_rfc3339()),
244                                ..Default::default()
245                            },
246                        ]),
247                        continuation_token: None,
248                    })
249                });
250        });
251        let store: KeyStore<KeyIds> = KeyStore::default();
252        #[allow(deprecated)]
253        let _ = store.context_mut().set_symmetric_key(
254            SymmetricKeyId::User,
255            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
256        );
257
258        let start_time = Utc::now();
259        let ciphers = restore_many_as_admin(
260            vec![
261                TEST_CIPHER_ID.parse().unwrap(),
262                TEST_CIPHER_ID_2.parse().unwrap(),
263            ],
264            TEST_ORG_ID.parse().unwrap(),
265            &api_client,
266            &store,
267        )
268        .await
269        .unwrap();
270        let end_time = Utc::now();
271
272        assert_eq!(ciphers.successes.len(), 2,);
273        assert_eq!(ciphers.failures.len(), 0,);
274        assert_eq!(
275            ciphers.successes[0].id,
276            Some(TEST_CIPHER_ID.parse().unwrap()),
277        );
278        assert_eq!(
279            ciphers.successes[1].id,
280            Some(TEST_CIPHER_ID_2.parse().unwrap()),
281        );
282        assert_eq!(ciphers.successes[0].deleted_date, None,);
283        assert_eq!(ciphers.successes[1].deleted_date, None,);
284
285        assert!(
286            ciphers.successes[0].revision_date >= start_time
287                && ciphers.successes[0].revision_date <= end_time
288        );
289        assert!(
290            ciphers.successes[1].revision_date >= start_time
291                && ciphers.successes[1].revision_date <= end_time
292        );
293    }
294}