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
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 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 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 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 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 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}