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