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