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