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