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