bitwarden_vault/cipher/cipher_client/admin/
restore.rs1use bitwarden_api_api::{apis::ApiClient, models::CipherBulkRestoreRequestModel};
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeyIds};
3use bitwarden_crypto::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10 Cipher, CipherId, CipherView, DecryptCipherListResult, VaultParseError,
11 cipher::cipher::PartialCipher, cipher_client::admin::CipherAdminClient,
12};
13
14#[allow(missing_docs)]
15#[bitwarden_error(flat)]
16#[derive(Debug, Error)]
17pub enum RestoreCipherAdminError {
18 #[error(transparent)]
19 Api(#[from] ApiError),
20 #[error(transparent)]
21 VaultParse(#[from] VaultParseError),
22 #[error(transparent)]
23 Crypto(#[from] CryptoError),
24}
25
26impl<T> From<bitwarden_api_api::apis::Error<T>> for RestoreCipherAdminError {
27 fn from(val: bitwarden_api_api::apis::Error<T>) -> Self {
28 Self::Api(val.into())
29 }
30}
31
32pub async fn restore_as_admin(
34 cipher_id: CipherId,
35 api_client: &ApiClient,
36 key_store: &KeyStore<KeyIds>,
37) -> Result<CipherView, RestoreCipherAdminError> {
38 let api = api_client.ciphers_api();
39
40 let cipher: Cipher = api
41 .put_restore_admin(cipher_id.into())
42 .await?
43 .merge_with_cipher(None)?;
44
45 Ok(key_store.decrypt(&cipher)?)
46}
47
48pub async fn restore_many_as_admin(
50 cipher_ids: Vec<CipherId>,
51 org_id: OrganizationId,
52 api_client: &ApiClient,
53 key_store: &KeyStore<KeyIds>,
54) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
55 let api = api_client.ciphers_api();
56
57 let ciphers: Vec<Cipher> = api
58 .put_restore_many_admin(Some(CipherBulkRestoreRequestModel {
59 ids: cipher_ids.into_iter().map(|id| id.to_string()).collect(),
60 organization_id: Some(org_id.into()),
61 }))
62 .await?
63 .data
64 .into_iter()
65 .flatten()
66 .map(|c| c.merge_with_cipher(None))
67 .collect::<Result<Vec<_>, _>>()?;
68
69 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
70 Ok(DecryptCipherListResult {
71 successes,
72 failures: failures.into_iter().cloned().collect(),
73 })
74}
75
76#[cfg_attr(feature = "wasm", wasm_bindgen)]
77impl CipherAdminClient {
78 pub async fn restore(
80 &self,
81 cipher_id: CipherId,
82 ) -> Result<CipherView, RestoreCipherAdminError> {
83 let api_client = &self
84 .client
85 .internal
86 .get_api_configurations()
87 .await
88 .api_client;
89 let key_store = self.client.internal.get_key_store();
90
91 restore_as_admin(cipher_id, api_client, key_store).await
92 }
93 pub async fn restore_many(
95 &self,
96 cipher_ids: Vec<CipherId>,
97 org_id: OrganizationId,
98 ) -> Result<DecryptCipherListResult, RestoreCipherAdminError> {
99 let api_client = &self
100 .client
101 .internal
102 .get_api_configurations()
103 .await
104 .api_client;
105 let key_store = self.client.internal.get_key_store();
106
107 restore_many_as_admin(cipher_ids, org_id, api_client, key_store).await
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use bitwarden_api_api::{
114 apis::ApiClient,
115 models::{CipherMiniResponseModel, CipherMiniResponseModelListResponseModel},
116 };
117 use bitwarden_core::key_management::{KeyIds, SymmetricKeyId};
118 use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
119 use chrono::Utc;
120
121 use super::*;
122 use crate::{Cipher, CipherId, Login};
123
124 const TEST_CIPHER_ID: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
125 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
126 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
127
128 fn generate_test_cipher() -> Cipher {
129 Cipher {
130 id: TEST_CIPHER_ID.parse().ok(),
131 name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
132 r#type: crate::CipherType::Login,
133 notes: Default::default(),
134 organization_id: Default::default(),
135 folder_id: Default::default(),
136 favorite: Default::default(),
137 reprompt: Default::default(),
138 fields: Default::default(),
139 collection_ids: Default::default(),
140 key: Default::default(),
141 login: Some(Login{
142 username: None,
143 password: None,
144 password_revision_date: None,
145 uris: None, totp: None,
146 autofill_on_page_load: None,
147 fido2_credentials: None,
148 }),
149 identity: Default::default(),
150 card: Default::default(),
151 secure_note: Default::default(),
152 ssh_key: Default::default(),
153 organization_use_totp: Default::default(),
154 edit: Default::default(),
155 permissions: Default::default(),
156 view_password: Default::default(),
157 local_data: Default::default(),
158 attachments: Default::default(),
159 password_history: Default::default(),
160 creation_date: Default::default(),
161 deleted_date: Default::default(),
162 revision_date: Default::default(),
163 archived_date: Default::default(),
164 data: Default::default(),
165 }
166 }
167
168 #[tokio::test]
169 async fn test_restore_as_admin() {
170 let mut cipher = generate_test_cipher();
171 cipher.deleted_date = Some(Utc::now());
172
173 let api_client = {
174 let cipher = cipher.clone();
175 ApiClient::new_mocked(move |mock| {
176 mock.ciphers_api
177 .expect_put_restore_admin()
178 .returning(move |_model| {
179 Ok(CipherMiniResponseModel {
180 id: Some(TEST_CIPHER_ID.try_into().unwrap()),
181 name: Some(cipher.name.to_string()),
182 r#type: Some(cipher.r#type.into()),
183 creation_date: Some(cipher.creation_date.to_string()),
184 revision_date: Some(Utc::now().to_rfc3339()),
185 login: cipher.login.clone().map(|l| Box::new(l.into())),
186 ..Default::default()
187 })
188 });
189 })
190 };
191
192 let store: KeyStore<KeyIds> = KeyStore::default();
193 #[allow(deprecated)]
194 let _ = store.context_mut().set_symmetric_key(
195 SymmetricKeyId::User,
196 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
197 );
198 let start_time = Utc::now();
199 let updated_cipher = restore_as_admin(TEST_CIPHER_ID.parse().unwrap(), &api_client, &store)
200 .await
201 .unwrap();
202 let end_time = Utc::now();
203
204 assert!(updated_cipher.deleted_date.is_none());
205 assert!(
206 updated_cipher.revision_date >= start_time && updated_cipher.revision_date <= end_time
207 );
208 }
209
210 #[tokio::test]
211 async fn test_restore_many_as_admin() {
212 let cipher_id_2: CipherId = TEST_CIPHER_ID_2.parse().unwrap();
213 let mut cipher_1 = generate_test_cipher();
214 cipher_1.deleted_date = Some(Utc::now());
215 let mut cipher_2 = generate_test_cipher();
216 cipher_2.deleted_date = Some(Utc::now());
217 cipher_2.id = Some(cipher_id_2);
218
219 let api_client = ApiClient::new_mocked(move |mock| {
220 mock.ciphers_api
221 .expect_put_restore_many_admin()
222 .returning(move |_model| {
223 Ok(CipherMiniResponseModelListResponseModel {
224 object: None,
225 data: Some(vec![
226 CipherMiniResponseModel {
227 id: cipher_1.id.map(|id| id.into()),
228 name: Some(cipher_1.name.to_string()),
229 r#type: Some(cipher_1.r#type.into()),
230 login: cipher_1.login.clone().map(|l| Box::new(l.into())),
231 creation_date: cipher_1.creation_date.to_string().into(),
232 deleted_date: None,
233 revision_date: Some(Utc::now().to_rfc3339()),
234 ..Default::default()
235 },
236 CipherMiniResponseModel {
237 id: cipher_2.id.map(|id| id.into()),
238 name: Some(cipher_2.name.to_string()),
239 r#type: Some(cipher_2.r#type.into()),
240 login: cipher_2.login.clone().map(|l| Box::new(l.into())),
241 creation_date: cipher_2.creation_date.to_string().into(),
242 deleted_date: None,
243 revision_date: Some(Utc::now().to_rfc3339()),
244 ..Default::default()
245 },
246 ]),
247 continuation_token: None,
248 })
249 });
250 });
251 let store: KeyStore<KeyIds> = KeyStore::default();
252 #[allow(deprecated)]
253 let _ = store.context_mut().set_symmetric_key(
254 SymmetricKeyId::User,
255 SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
256 );
257
258 let start_time = Utc::now();
259 let ciphers = restore_many_as_admin(
260 vec![
261 TEST_CIPHER_ID.parse().unwrap(),
262 TEST_CIPHER_ID_2.parse().unwrap(),
263 ],
264 TEST_ORG_ID.parse().unwrap(),
265 &api_client,
266 &store,
267 )
268 .await
269 .unwrap();
270 let end_time = Utc::now();
271
272 assert_eq!(ciphers.successes.len(), 2,);
273 assert_eq!(ciphers.failures.len(), 0,);
274 assert_eq!(
275 ciphers.successes[0].id,
276 Some(TEST_CIPHER_ID.parse().unwrap()),
277 );
278 assert_eq!(
279 ciphers.successes[1].id,
280 Some(TEST_CIPHER_ID_2.parse().unwrap()),
281 );
282 assert_eq!(ciphers.successes[0].deleted_date, None,);
283 assert_eq!(ciphers.successes[1].deleted_date, None,);
284
285 assert!(
286 ciphers.successes[0].revision_date >= start_time
287 && ciphers.successes[0].revision_date <= end_time
288 );
289 assert!(
290 ciphers.successes[1].revision_date >= start_time
291 && ciphers.successes[1].revision_date <= end_time
292 );
293 }
294}