Skip to main content

bitwarden_vault/cipher/cipher_client/
delete.rs

1use bitwarden_api_api::models::CipherBulkDeleteRequestModel;
2use bitwarden_core::{ApiError, OrganizationId};
3use bitwarden_error::bitwarden_error;
4use bitwarden_state::repository::{Repository, RepositoryError};
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{Cipher, CipherId, CiphersClient};
10
11#[allow(missing_docs)]
12#[bitwarden_error(flat)]
13#[derive(Debug, Error)]
14pub enum DeleteCipherError {
15    #[error(transparent)]
16    Api(#[from] ApiError),
17    #[error(transparent)]
18    Repository(#[from] RepositoryError),
19}
20
21async fn delete_cipher<R: Repository<Cipher> + ?Sized>(
22    cipher_id: CipherId,
23    api_client: &bitwarden_api_api::apis::ApiClient,
24    repository: &R,
25) -> Result<(), DeleteCipherError> {
26    let api = api_client.ciphers_api();
27    api.delete(cipher_id.into()).await?;
28    repository.remove(cipher_id).await?;
29    Ok(())
30}
31
32async fn delete_ciphers<R: Repository<Cipher> + ?Sized>(
33    cipher_ids: Vec<CipherId>,
34    organization_id: Option<OrganizationId>,
35    api_client: &bitwarden_api_api::apis::ApiClient,
36    repository: &R,
37) -> Result<(), DeleteCipherError> {
38    let api = api_client.ciphers_api();
39
40    api.delete_many(Some(CipherBulkDeleteRequestModel {
41        ids: cipher_ids.iter().map(|id| id.to_string()).collect(),
42        organization_id: organization_id.map(|id| id.to_string()),
43    }))
44    .await?;
45
46    for cipher_id in cipher_ids {
47        repository.remove(cipher_id).await?;
48    }
49    Ok(())
50}
51
52async fn soft_delete<R: Repository<Cipher> + ?Sized>(
53    cipher_id: CipherId,
54    api_client: &bitwarden_api_api::apis::ApiClient,
55    repository: &R,
56) -> Result<(), DeleteCipherError> {
57    let api = api_client.ciphers_api();
58    api.put_delete(cipher_id.into()).await?;
59    process_soft_delete(repository, cipher_id).await?;
60    Ok(())
61}
62
63async fn soft_delete_many<R: Repository<Cipher> + ?Sized>(
64    cipher_ids: Vec<CipherId>,
65    organization_id: Option<OrganizationId>,
66    api_client: &bitwarden_api_api::apis::ApiClient,
67    repository: &R,
68) -> Result<(), DeleteCipherError> {
69    let api = api_client.ciphers_api();
70
71    api.put_delete_many(Some(CipherBulkDeleteRequestModel {
72        ids: cipher_ids.iter().map(|id| id.to_string()).collect(),
73        organization_id: organization_id.map(|id| id.to_string()),
74    }))
75    .await?;
76    for cipher_id in cipher_ids {
77        process_soft_delete(repository, cipher_id).await?;
78    }
79    Ok(())
80}
81
82async fn process_soft_delete<R: Repository<Cipher> + ?Sized>(
83    repository: &R,
84    cipher_id: CipherId,
85) -> Result<(), RepositoryError> {
86    let cipher: Option<Cipher> = repository.get(cipher_id).await?;
87    if let Some(mut cipher) = cipher {
88        cipher.soft_delete();
89        repository.set(cipher_id, cipher).await?;
90    }
91    Ok(())
92}
93
94#[allow(deprecated)]
95#[cfg_attr(feature = "wasm", wasm_bindgen)]
96impl CiphersClient {
97    /// Deletes the [Cipher] with the matching [CipherId] from the server.
98    pub async fn delete(&self, cipher_id: CipherId) -> Result<(), DeleteCipherError> {
99        let configs = self.client.internal.get_api_configurations();
100        delete_cipher(cipher_id, &configs.api_client, &*self.get_repository()?).await
101    }
102
103    /// Deletes all [Cipher] objects with a matching [CipherId] from the server.
104    pub async fn delete_many(
105        &self,
106        cipher_ids: Vec<CipherId>,
107        organization_id: Option<OrganizationId>,
108    ) -> Result<(), DeleteCipherError> {
109        let configs = self.client.internal.get_api_configurations();
110        delete_ciphers(
111            cipher_ids,
112            organization_id,
113            &configs.api_client,
114            &*self.get_repository()?,
115        )
116        .await
117    }
118
119    /// Soft-deletes the [Cipher] with the matching [CipherId] from the server.
120    pub async fn soft_delete(&self, cipher_id: CipherId) -> Result<(), DeleteCipherError> {
121        let configs = self.client.internal.get_api_configurations();
122        soft_delete(cipher_id, &configs.api_client, &*self.get_repository()?).await
123    }
124
125    /// Soft-deletes all [Cipher] objects for the given [CipherId]s from the server.
126    pub async fn soft_delete_many(
127        &self,
128        cipher_ids: Vec<CipherId>,
129        organization_id: Option<OrganizationId>,
130    ) -> Result<(), DeleteCipherError> {
131        soft_delete_many(
132            cipher_ids,
133            organization_id,
134            &self.client.internal.get_api_configurations().api_client,
135            &*self.get_repository()?,
136        )
137        .await
138    }
139}
140
141#[cfg(test)]
142mod tests {
143    use bitwarden_api_api::apis::ApiClient;
144    use bitwarden_state::repository::Repository;
145    use bitwarden_test::MemoryRepository;
146    use chrono::Utc;
147
148    use crate::{
149        Cipher, CipherId,
150        cipher_client::delete::{delete_cipher, delete_ciphers, soft_delete, soft_delete_many},
151    };
152
153    const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
154    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
155
156    fn generate_test_cipher() -> Cipher {
157        Cipher {
158            partial_data: None,
159            id: TEST_CIPHER_ID.parse().ok(),
160            name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap()),
161            r#type: crate::CipherType::Login,
162            notes: Default::default(),
163            organization_id: Default::default(),
164            folder_id: Default::default(),
165            favorite: Default::default(),
166            reprompt: Default::default(),
167            fields: Default::default(),
168            collection_ids: Default::default(),
169            key: Default::default(),
170            login: Default::default(),
171            identity: Default::default(),
172            card: Default::default(),
173            secure_note: Default::default(),
174            ssh_key: Default::default(),
175            bank_account: Default::default(),
176            drivers_license: Default::default(),
177            passport: Default::default(),
178            organization_use_totp: Default::default(),
179            edit: Default::default(),
180            permissions: Default::default(),
181            view_password: Default::default(),
182            local_data: Default::default(),
183            attachments: Default::default(),
184            password_history: Default::default(),
185            creation_date: Default::default(),
186            deleted_date: Default::default(),
187            revision_date: Default::default(),
188            archived_date: Default::default(),
189            data: Default::default(),
190        }
191    }
192
193    #[tokio::test]
194    async fn test_delete() {
195        let api_client = ApiClient::new_mocked(move |mock| {
196            mock.ciphers_api
197                .expect_delete()
198                .returning(move |_model| Ok(()));
199        });
200
201        // let client = create_client_with_wiremock(mock_server).await;
202        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
203        let repository = MemoryRepository::<Cipher>::default();
204        repository
205            .set(cipher_id, generate_test_cipher())
206            .await
207            .unwrap();
208
209        delete_cipher(cipher_id, &api_client, &repository)
210            .await
211            .unwrap();
212
213        let cipher = repository.get(cipher_id).await.unwrap();
214        assert!(
215            cipher.is_none(),
216            "Cipher is deleted from the local repository"
217        );
218    }
219
220    #[tokio::test]
221    async fn test_delete_many() {
222        let api_client = ApiClient::new_mocked(move |mock| {
223            mock.ciphers_api
224                .expect_delete_many()
225                .returning(move |_model| Ok(()));
226        });
227        let repository = MemoryRepository::<Cipher>::default();
228
229        let cipher_1 = generate_test_cipher();
230        let mut cipher_2 = generate_test_cipher();
231        cipher_2.id = Some(TEST_CIPHER_ID_2.parse().unwrap());
232
233        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
234        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
235
236        repository.set(cipher_id, cipher_1).await.unwrap();
237        repository.set(cipher_id_2, cipher_2).await.unwrap();
238
239        delete_ciphers(vec![cipher_id, cipher_id_2], None, &api_client, &repository)
240            .await
241            .unwrap();
242
243        let cipher_1 = repository.get(cipher_id).await.unwrap();
244        let cipher_2 = repository.get(cipher_id_2).await.unwrap();
245        assert!(
246            cipher_1.is_none(),
247            "Cipher is deleted from the local repository"
248        );
249        assert!(
250            cipher_2.is_none(),
251            "Cipher is deleted from the local repository"
252        );
253    }
254
255    #[tokio::test]
256    async fn test_soft_delete() {
257        let api_client = ApiClient::new_mocked(move |mock| {
258            mock.ciphers_api
259                .expect_put_delete()
260                .returning(move |_model| Ok(()));
261        });
262        let repository = MemoryRepository::<Cipher>::default();
263
264        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
265        repository
266            .set(cipher_id, generate_test_cipher())
267            .await
268            .unwrap();
269
270        let start_time = Utc::now();
271        soft_delete(cipher_id, &api_client, &repository)
272            .await
273            .unwrap();
274        let end_time = Utc::now();
275
276        let cipher: Cipher = repository.get(cipher_id).await.unwrap().unwrap();
277        assert!(
278            cipher.deleted_date.unwrap() >= start_time && cipher.deleted_date.unwrap() <= end_time,
279            "Cipher was flagged as deleted in the repository."
280        );
281    }
282
283    #[tokio::test]
284    async fn test_soft_delete_many() {
285        let api_client = ApiClient::new_mocked(move |mock| {
286            mock.ciphers_api
287                .expect_put_delete_many()
288                .returning(move |_model| Ok(()));
289        });
290        let repository = MemoryRepository::<Cipher>::default();
291
292        let cipher_1 = generate_test_cipher();
293        let mut cipher_2 = generate_test_cipher();
294        cipher_2.id = Some(TEST_CIPHER_ID_2.parse().unwrap());
295
296        let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
297        let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
298        repository.set(cipher_id, cipher_1).await.unwrap();
299        repository.set(cipher_id_2, cipher_2).await.unwrap();
300
301        let start_time = Utc::now();
302
303        soft_delete_many(vec![cipher_id, cipher_id_2], None, &api_client, &repository)
304            .await
305            .unwrap();
306        let end_time = Utc::now();
307
308        let cipher_1 = repository.get(cipher_id).await.unwrap().unwrap();
309        let cipher_2 = repository.get(cipher_id_2).await.unwrap().unwrap();
310
311        assert!(
312            cipher_1.deleted_date.unwrap() >= start_time
313                && cipher_1.deleted_date.unwrap() <= end_time,
314            "Cipher was flagged as deleted in the repository."
315        );
316        assert!(
317            cipher_2.deleted_date.unwrap() >= start_time
318                && cipher_2.deleted_date.unwrap() <= end_time,
319            "Cipher was flagged as deleted in the repository."
320        );
321    }
322}