1use bitwarden_api_api::models::CipherMiniDetailsResponseModelListResponseModel;
2use bitwarden_core::{ApiError, OrganizationId, key_management::KeySlotIds};
3use bitwarden_crypto::KeyStore;
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10 Cipher, VaultParseError,
11 cipher::cipher::{
12 DecryptCipherResult, ListOrganizationCiphersResult, PartialCipher, StrictDecrypt,
13 },
14 cipher_client::admin::CipherAdminClient,
15};
16
17#[allow(missing_docs)]
18#[bitwarden_error(flat)]
19#[derive(Debug, Error)]
20pub enum GetAssignedOrgCiphersAdminError {
21 #[error(transparent)]
22 Api(#[from] ApiError),
23 #[error(transparent)]
24 VaultParse(#[from] VaultParseError),
25}
26
27#[allow(missing_docs)]
28#[bitwarden_error(flat)]
29#[derive(Debug, Error)]
30pub enum GetOrganizationCiphersAdminError {
31 #[error(transparent)]
32 VaultParse(#[from] VaultParseError),
33 #[error(transparent)]
34 Api(#[from] ApiError),
35}
36
37pub async fn list_org_ciphers(
39 org_id: OrganizationId,
40 include_member_items: bool,
41 api_client: &bitwarden_api_api::apis::ApiClient,
42 key_store: &KeyStore<KeySlotIds>,
43 use_strict_decryption: bool,
44) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
45 let api = api_client.ciphers_api();
46 let response: CipherMiniDetailsResponseModelListResponseModel = api
47 .get_organization_ciphers(Some(org_id.into()), Some(include_member_items))
48 .await?;
49 let ciphers = response
50 .data
51 .into_iter()
52 .flatten()
53 .map(|model| model.merge_with_cipher(None))
54 .collect::<Result<Vec<_>, _>>()?;
55
56 let list_views = if use_strict_decryption {
57 let wrapped: Vec<StrictDecrypt<Cipher>> =
58 ciphers.iter().cloned().map(StrictDecrypt).collect();
59 let (list_views, _failures) = key_store.decrypt_list_with_failures(&wrapped);
60 list_views
61 } else {
62 let (list_views, _failures) = key_store.decrypt_list_with_failures(&ciphers);
63 list_views
64 };
65 Ok(ListOrganizationCiphersResult {
66 ciphers,
67 list_views,
68 })
69}
70
71pub async fn list_org_login_ciphers(
73 org_id: OrganizationId,
74 api_client: &bitwarden_api_api::apis::ApiClient,
75 key_store: &KeyStore<KeySlotIds>,
76 use_strict_decryption: bool,
77) -> Result<DecryptCipherResult, GetOrganizationCiphersAdminError> {
78 let response: CipherMiniDetailsResponseModelListResponseModel = api_client
79 .ciphers_api()
80 .get_organization_login_ciphers(Some(org_id.into()))
81 .await?;
82 let ciphers = response
83 .data
84 .into_iter()
85 .flatten()
86 .map(|model| model.merge_with_cipher(None))
87 .collect::<Result<Vec<_>, _>>()?;
88
89 Ok(if use_strict_decryption {
90 let wrapped: Vec<StrictDecrypt<Cipher>> = ciphers.into_iter().map(StrictDecrypt).collect();
91 let (successes, failures) = key_store.decrypt_list_with_failures(&wrapped);
92 DecryptCipherResult {
93 successes,
94 failures: failures.into_iter().map(|f| f.0.clone()).collect(),
95 }
96 } else {
97 let (successes, failures) = key_store.decrypt_list_with_failures(&ciphers);
98 DecryptCipherResult {
99 successes,
100 failures: failures.into_iter().cloned().collect(),
101 }
102 })
103}
104
105#[cfg_attr(feature = "wasm", wasm_bindgen)]
106impl CipherAdminClient {
107 pub async fn list_assigned_org_ciphers(
109 &self,
110 org_id: OrganizationId,
111 ) -> Result<ListOrganizationCiphersResult, GetAssignedOrgCiphersAdminError> {
112 use bitwarden_api_api::models::CipherDetailsResponseModelListResponseModel;
113
114 let response: CipherDetailsResponseModelListResponseModel = self
115 .api_configurations
116 .api_client
117 .ciphers_api()
118 .get_assigned_organization_ciphers(Some(org_id.into()))
119 .await?;
120
121 let ciphers = response
122 .data
123 .into_iter()
124 .flatten()
125 .map(|model| model.merge_with_cipher(None))
126 .collect::<Result<Vec<_>, _>>()?;
127
128 let list_views = if self.is_strict_decrypt().await {
129 let wrapped: Vec<StrictDecrypt<Cipher>> =
130 ciphers.iter().cloned().map(StrictDecrypt).collect();
131 let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&wrapped);
132 list_views
133 } else {
134 let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&ciphers);
135 list_views
136 };
137 Ok(ListOrganizationCiphersResult {
138 ciphers,
139 list_views,
140 })
141 }
142
143 pub async fn list_org_ciphers(
145 &self,
146 org_id: OrganizationId,
147 include_member_items: bool,
148 ) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
149 list_org_ciphers(
150 org_id,
151 include_member_items,
152 &self.api_configurations.api_client,
153 &self.key_store,
154 self.is_strict_decrypt().await,
155 )
156 .await
157 }
158
159 pub async fn list_org_login_ciphers(
161 &self,
162 org_id: OrganizationId,
163 ) -> Result<DecryptCipherResult, GetOrganizationCiphersAdminError> {
164 list_org_login_ciphers(
165 org_id,
166 &self.api_configurations.api_client,
167 &self.key_store,
168 self.is_strict_decrypt().await,
169 )
170 .await
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use std::sync::Arc;
177
178 use bitwarden_api_api::{
179 apis::ApiClient,
180 models::{
181 CipherDetailsResponseModel, CipherDetailsResponseModelListResponseModel,
182 CipherMiniDetailsResponseModel, CipherMiniDetailsResponseModelListResponseModel,
183 },
184 };
185 use bitwarden_core::{
186 client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
187 };
188 use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
189 use chrono::Utc;
190
191 use super::*;
192 use crate::{Cipher, CipherType, Login};
193
194 const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
195 const TEST_CIPHER_ID_1: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
196 const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
197
198 fn create_test_client(api_client: ApiClient) -> CipherAdminClient {
199 #[allow(deprecated)]
200 CipherAdminClient {
201 key_store: create_test_crypto_with_user_key(SymmetricCryptoKey::make(
202 SymmetricKeyAlgorithm::Aes256CbcHmac,
203 )),
204 api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
205 client: bitwarden_core::Client::new_test(None),
206 }
207 }
208
209 fn mock_mini_cipher(cipher_id: &str) -> CipherMiniDetailsResponseModel {
210 let cipher = generate_test_cipher();
211 CipherMiniDetailsResponseModel {
212 id: cipher_id.parse().ok(),
213 name: cipher.name.as_ref().map(ToString::to_string),
214 r#type: Some(cipher.r#type.into()),
215 login: cipher.login.clone().map(|l| Box::new(l.into())),
216 creation_date: Some(Utc::now().to_rfc3339()),
217 revision_date: Some(Utc::now().to_rfc3339()),
218 ..Default::default()
219 }
220 }
221
222 fn mock_details_cipher(cipher_id: &str) -> CipherDetailsResponseModel {
223 CipherDetailsResponseModel {
224 id: Some(cipher_id.parse().unwrap()),
225 name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".to_string()),
226 r#type: Some(bitwarden_api_api::models::CipherType::Login),
227 login: Some(Box::new(bitwarden_api_api::models::CipherLoginModel::default())),
228 creation_date: Some(Utc::now().to_rfc3339()),
229 revision_date: Some(Utc::now().to_rfc3339()),
230 ..Default::default()
231 }
232 }
233
234 fn generate_test_cipher() -> Cipher {
235 Cipher {
236 partial_data: None,
237 id: TEST_CIPHER_ID_1.parse().ok(),
238 name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap()),
239 r#type: CipherType::Login,
240 notes: Default::default(),
241 organization_id: Default::default(),
242 folder_id: Default::default(),
243 favorite: Default::default(),
244 reprompt: Default::default(),
245 fields: Default::default(),
246 collection_ids: Default::default(),
247 key: Default::default(),
248 login: Some(Login {
249 username: None,
250 password: None,
251 password_revision_date: None,
252 uris: None,
253 totp: None,
254 autofill_on_page_load: None,
255 fido2_credentials: None,
256 }),
257 identity: Default::default(),
258 card: Default::default(),
259 secure_note: Default::default(),
260 ssh_key: Default::default(),
261 bank_account: Default::default(),
262 drivers_license: Default::default(),
263 passport: Default::default(),
264 organization_use_totp: Default::default(),
265 edit: Default::default(),
266 permissions: Default::default(),
267 view_password: Default::default(),
268 local_data: Default::default(),
269 attachments: Default::default(),
270 password_history: Default::default(),
271 creation_date: Default::default(),
272 deleted_date: Default::default(),
273 revision_date: Default::default(),
274 archived_date: Default::default(),
275 data: Default::default(),
276 }
277 }
278
279 #[tokio::test]
280 async fn test_list_org_ciphers_all_success() {
281 let api_client = ApiClient::new_mocked(move |mock| {
282 mock.ciphers_api
283 .expect_get_organization_ciphers()
284 .returning(move |_org_id, _include_member_items| {
285 Ok(CipherMiniDetailsResponseModelListResponseModel {
286 object: None,
287 data: Some(vec![
288 mock_mini_cipher(TEST_CIPHER_ID_1),
289 mock_mini_cipher(TEST_CIPHER_ID_2),
290 ]),
291 continuation_token: None,
292 })
293 });
294 });
295
296 let client = create_test_client(api_client);
297 let result = client
298 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
299 .await
300 .unwrap();
301
302 assert_eq!(result.ciphers.len(), 2);
303 assert_eq!(result.list_views.len(), 2);
304 assert_eq!(result.ciphers[0].id, TEST_CIPHER_ID_1.parse().ok());
305 assert_eq!(result.ciphers[1].id, TEST_CIPHER_ID_2.parse().ok());
306 }
307
308 #[tokio::test]
309 async fn test_list_org_ciphers_with_failures() {
310 let api_client = ApiClient::new_mocked(move |mock| {
311 mock.ciphers_api
312 .expect_get_organization_ciphers()
313 .returning(move |_org_id, _include_member_items| {
314 let mut bad = mock_mini_cipher(TEST_CIPHER_ID_2);
315 bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
316 Ok(CipherMiniDetailsResponseModelListResponseModel {
317 object: None,
318 data: Some(vec![mock_mini_cipher(TEST_CIPHER_ID_1), bad]),
319 continuation_token: None,
320 })
321 });
322 });
323
324 let client = create_test_client(api_client);
325 let result = client
326 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
327 .await
328 .unwrap();
329
330 assert_eq!(result.ciphers.len(), 2);
331 assert_eq!(result.list_views.len(), 1);
332 }
333
334 #[tokio::test]
335 async fn test_list_org_ciphers_empty() {
336 let api_client = ApiClient::new_mocked(move |mock| {
337 mock.ciphers_api
338 .expect_get_organization_ciphers()
339 .returning(move |_org_id, _include_member_items| {
340 Ok(CipherMiniDetailsResponseModelListResponseModel {
341 object: None,
342 data: Some(vec![]),
343 continuation_token: None,
344 })
345 });
346 });
347
348 let client = create_test_client(api_client);
349 let result = client
350 .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), false)
351 .await
352 .unwrap();
353
354 assert!(result.ciphers.is_empty());
355 assert!(result.list_views.is_empty());
356 }
357
358 #[tokio::test]
359 async fn test_list_org_login_ciphers_all_success() {
360 let api_client = ApiClient::new_mocked(move |mock| {
361 mock.ciphers_api
362 .expect_get_organization_login_ciphers()
363 .withf(|org_id| *org_id == TEST_ORG_ID.parse().ok())
364 .returning(move |_org_id| {
365 Ok(CipherMiniDetailsResponseModelListResponseModel {
366 object: None,
367 data: Some(vec![
368 mock_mini_cipher(TEST_CIPHER_ID_1),
369 mock_mini_cipher(TEST_CIPHER_ID_2),
370 ]),
371 continuation_token: None,
372 })
373 });
374 });
375
376 let client = create_test_client(api_client);
377 let result = client
378 .list_org_login_ciphers(TEST_ORG_ID.parse().unwrap())
379 .await
380 .unwrap();
381
382 assert_eq!(result.successes.len(), 2);
383 assert!(result.failures.is_empty());
384 assert_eq!(result.successes[0].id, TEST_CIPHER_ID_1.parse().ok());
385 assert_eq!(result.successes[1].id, TEST_CIPHER_ID_2.parse().ok());
386 assert!(result.successes[0].login.is_some());
387 }
388
389 #[tokio::test]
390 async fn test_list_org_login_ciphers_with_failures() {
391 let api_client = ApiClient::new_mocked(move |mock| {
392 mock.ciphers_api
393 .expect_get_organization_login_ciphers()
394 .returning(move |_org_id| {
395 let mut bad = mock_mini_cipher(TEST_CIPHER_ID_2);
396 bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
397 Ok(CipherMiniDetailsResponseModelListResponseModel {
398 object: None,
399 data: Some(vec![mock_mini_cipher(TEST_CIPHER_ID_1), bad]),
400 continuation_token: None,
401 })
402 });
403 });
404
405 let client = create_test_client(api_client);
406 let result = client
407 .list_org_login_ciphers(TEST_ORG_ID.parse().unwrap())
408 .await
409 .unwrap();
410
411 assert_eq!(result.successes.len(), 1);
412 assert_eq!(result.successes[0].id, TEST_CIPHER_ID_1.parse().ok());
413 assert_eq!(result.failures.len(), 1);
414 assert_eq!(result.failures[0].id, TEST_CIPHER_ID_2.parse().ok());
415 }
416
417 #[tokio::test]
418 async fn test_list_org_login_ciphers_empty() {
419 let api_client = ApiClient::new_mocked(move |mock| {
420 mock.ciphers_api
421 .expect_get_organization_login_ciphers()
422 .returning(move |_org_id| {
423 Ok(CipherMiniDetailsResponseModelListResponseModel {
424 object: None,
425 data: None,
426 continuation_token: None,
427 })
428 });
429 });
430
431 let client = create_test_client(api_client);
432 let result = client
433 .list_org_login_ciphers(TEST_ORG_ID.parse().unwrap())
434 .await
435 .unwrap();
436
437 assert!(result.successes.is_empty());
438 assert!(result.failures.is_empty());
439 }
440
441 #[tokio::test]
442 async fn test_list_assigned_org_ciphers_success() {
443 let api_client = ApiClient::new_mocked(|mock| {
444 mock.ciphers_api
445 .expect_get_assigned_organization_ciphers()
446 .returning(|_| {
447 Ok(CipherDetailsResponseModelListResponseModel {
448 object: None,
449 data: Some(vec![
450 mock_details_cipher(TEST_CIPHER_ID_1),
451 mock_details_cipher(TEST_CIPHER_ID_2),
452 ]),
453 continuation_token: None,
454 })
455 });
456 });
457
458 let client = create_test_client(api_client);
459 let result = client
460 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
461 .await
462 .unwrap();
463
464 assert_eq!(result.ciphers.len(), 2);
465 assert_eq!(result.list_views.len(), 2);
466 }
467
468 #[tokio::test]
469 async fn test_list_assigned_org_ciphers_with_failures() {
470 let api_client = ApiClient::new_mocked(|mock| {
471 mock.ciphers_api
472 .expect_get_assigned_organization_ciphers()
473 .returning(|_| {
474 let mut bad = mock_details_cipher(TEST_CIPHER_ID_2);
475 bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
476 Ok(CipherDetailsResponseModelListResponseModel {
477 object: None,
478 data: Some(vec![mock_details_cipher(TEST_CIPHER_ID_1), bad]),
479 continuation_token: None,
480 })
481 });
482 });
483
484 let client = create_test_client(api_client);
485 let result = client
486 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
487 .await
488 .unwrap();
489
490 assert_eq!(result.ciphers.len(), 2);
491 assert_eq!(result.list_views.len(), 1);
492 assert_eq!(result.list_views[0].id, TEST_CIPHER_ID_1.parse().ok());
493 }
494
495 #[tokio::test]
496 async fn test_list_assigned_org_ciphers_empty() {
497 let api_client = ApiClient::new_mocked(|mock| {
498 mock.ciphers_api
499 .expect_get_assigned_organization_ciphers()
500 .returning(|_| {
501 Ok(CipherDetailsResponseModelListResponseModel {
502 object: None,
503 data: Some(vec![]),
504 continuation_token: None,
505 })
506 });
507 });
508
509 let client = create_test_client(api_client);
510 let result = client
511 .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
512 .await
513 .unwrap();
514
515 assert!(result.ciphers.is_empty());
516 assert!(result.list_views.is_empty());
517 }
518}