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