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