bitwarden_vault/cipher/cipher_client/
delete.rs1use 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 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 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 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 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 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}