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
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.to_string()).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.to_string()).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.to_string()).await?;
93 if let Some(mut cipher) = cipher {
94 cipher.soft_delete();
95 repository.set(cipher_id.to_string(), cipher).await?;
96 }
97 Ok(())
98}
99
100#[cfg_attr(feature = "wasm", wasm_bindgen)]
101impl CiphersClient {
102 pub async fn delete(&self, cipher_id: CipherId) -> Result<(), DeleteCipherError> {
104 let configs = self.client.internal.get_api_configurations().await;
105 delete_cipher(cipher_id, &configs.api_client, &*self.get_repository()?).await
106 }
107
108 pub async fn delete_many(
110 &self,
111 cipher_ids: Vec<CipherId>,
112 organization_id: Option<OrganizationId>,
113 ) -> Result<(), DeleteCipherError> {
114 let configs = self.client.internal.get_api_configurations().await;
115 delete_ciphers(
116 cipher_ids,
117 organization_id,
118 &configs.api_client,
119 &*self.get_repository()?,
120 )
121 .await
122 }
123
124 pub async fn soft_delete(&self, cipher_id: CipherId) -> Result<(), DeleteCipherError> {
126 let configs = self.client.internal.get_api_configurations().await;
127 soft_delete(cipher_id, &configs.api_client, &*self.get_repository()?).await
128 }
129
130 pub async fn soft_delete_many(
132 &self,
133 cipher_ids: Vec<CipherId>,
134 organization_id: Option<OrganizationId>,
135 ) -> Result<(), DeleteCipherError> {
136 soft_delete_many(
137 cipher_ids,
138 organization_id,
139 &self
140 .client
141 .internal
142 .get_api_configurations()
143 .await
144 .api_client,
145 &*self.get_repository()?,
146 )
147 .await
148 }
149}
150
151#[cfg(test)]
152mod tests {
153 use bitwarden_api_api::apis::ApiClient;
154 use bitwarden_state::repository::Repository;
155 use bitwarden_test::MemoryRepository;
156 use chrono::Utc;
157
158 use crate::{
159 Cipher, CipherId,
160 cipher_client::delete::{delete_cipher, delete_ciphers, soft_delete, soft_delete_many},
161 };
162
163 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
164 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
165
166 fn generate_test_cipher() -> Cipher {
167 Cipher {
168 id: TEST_CIPHER_ID.parse().ok(),
169 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
170 r#type: crate::CipherType::Login,
171 notes: Default::default(),
172 organization_id: Default::default(),
173 folder_id: Default::default(),
174 favorite: Default::default(),
175 reprompt: Default::default(),
176 fields: Default::default(),
177 collection_ids: Default::default(),
178 key: Default::default(),
179 login: Default::default(),
180 identity: Default::default(),
181 card: Default::default(),
182 secure_note: Default::default(),
183 ssh_key: Default::default(),
184 organization_use_totp: Default::default(),
185 edit: Default::default(),
186 permissions: Default::default(),
187 view_password: Default::default(),
188 local_data: Default::default(),
189 attachments: Default::default(),
190 password_history: Default::default(),
191 creation_date: Default::default(),
192 deleted_date: Default::default(),
193 revision_date: Default::default(),
194 archived_date: Default::default(),
195 data: Default::default(),
196 }
197 }
198
199 #[tokio::test]
200 async fn test_delete() {
201 let api_client = ApiClient::new_mocked(move |mock| {
202 mock.ciphers_api
203 .expect_delete()
204 .returning(move |_model| Ok(()));
205 });
206
207 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
209 let repository = MemoryRepository::<Cipher>::default();
210 repository
211 .set(cipher_id.to_string(), generate_test_cipher())
212 .await
213 .unwrap();
214
215 delete_cipher(cipher_id, &api_client, &repository)
216 .await
217 .unwrap();
218
219 let cipher = repository.get(cipher_id.to_string()).await.unwrap();
220 assert!(
221 cipher.is_none(),
222 "Cipher is deleted from the local repository"
223 );
224 }
225
226 #[tokio::test]
227 async fn test_delete_many() {
228 let api_client = ApiClient::new_mocked(move |mock| {
229 mock.ciphers_api
230 .expect_delete_many()
231 .returning(move |_model| Ok(()));
232 });
233 let repository = MemoryRepository::<Cipher>::default();
234
235 let cipher_1 = generate_test_cipher();
236 let mut cipher_2 = generate_test_cipher();
237 cipher_2.id = Some(TEST_CIPHER_ID_2.parse().unwrap());
238
239 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
240 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
241
242 repository
243 .set(cipher_id.to_string(), cipher_1)
244 .await
245 .unwrap();
246 repository
247 .set(TEST_CIPHER_ID_2.to_string(), cipher_2)
248 .await
249 .unwrap();
250
251 delete_ciphers(vec![cipher_id, cipher_id_2], None, &api_client, &repository)
252 .await
253 .unwrap();
254
255 let cipher_1 = repository.get(cipher_id.to_string()).await.unwrap();
256 let cipher_2 = repository.get(cipher_id_2.to_string()).await.unwrap();
257 assert!(
258 cipher_1.is_none(),
259 "Cipher is deleted from the local repository"
260 );
261 assert!(
262 cipher_2.is_none(),
263 "Cipher is deleted from the local repository"
264 );
265 }
266
267 #[tokio::test]
268 async fn test_soft_delete() {
269 let api_client = ApiClient::new_mocked(move |mock| {
270 mock.ciphers_api
271 .expect_put_delete()
272 .returning(move |_model| Ok(()));
273 });
274 let repository = MemoryRepository::<Cipher>::default();
275
276 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
277 repository
278 .set(cipher_id.to_string(), generate_test_cipher())
279 .await
280 .unwrap();
281
282 let start_time = Utc::now();
283 soft_delete(cipher_id, &api_client, &repository)
284 .await
285 .unwrap();
286 let end_time = Utc::now();
287
288 let cipher: Cipher = repository
289 .get(cipher_id.to_string())
290 .await
291 .unwrap()
292 .unwrap();
293 assert!(
294 cipher.deleted_date.unwrap() >= start_time && cipher.deleted_date.unwrap() <= end_time,
295 "Cipher was flagged as deleted in the repository."
296 );
297 }
298
299 #[tokio::test]
300 async fn test_soft_delete_many() {
301 let api_client = ApiClient::new_mocked(move |mock| {
302 mock.ciphers_api
303 .expect_put_delete_many()
304 .returning(move |_model| Ok(()));
305 });
306 let repository = MemoryRepository::<Cipher>::default();
307
308 let cipher_1 = generate_test_cipher();
309 let mut cipher_2 = generate_test_cipher();
310 cipher_2.id = Some(TEST_CIPHER_ID_2.parse().unwrap());
311
312 let cipher_id: CipherId = TEST_CIPHER_ID.parse().unwrap();
313 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
314 repository
315 .set(cipher_id.to_string(), cipher_1)
316 .await
317 .unwrap();
318 repository
319 .set(TEST_CIPHER_ID_2.to_string(), cipher_2)
320 .await
321 .unwrap();
322
323 let start_time = Utc::now();
324
325 soft_delete_many(vec![cipher_id, cipher_id_2], None, &api_client, &repository)
326 .await
327 .unwrap();
328 let end_time = Utc::now();
329
330 let cipher_1 = repository
331 .get(cipher_id.to_string())
332 .await
333 .unwrap()
334 .unwrap();
335 let cipher_2 = repository
336 .get(cipher_id_2.to_string())
337 .await
338 .unwrap()
339 .unwrap();
340
341 assert!(
342 cipher_1.deleted_date.unwrap() >= start_time
343 && cipher_1.deleted_date.unwrap() <= end_time,
344 "Cipher was flagged as deleted in the repository."
345 );
346 assert!(
347 cipher_2.deleted_date.unwrap() >= start_time
348 && cipher_2.deleted_date.unwrap() <= end_time,
349 "Cipher was flagged as deleted in the repository."
350 );
351 }
352}