Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
get.rs

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
37/// Get all ciphers for an organization.
38pub 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
71/// Get all Login ciphers for an organization, decrypted to full [crate::CipherView].
72pub 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    /// Fetches and decrypts all ciphers assigned to the current user for an organization.
108    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    /// Get all ciphers for an organization.
144    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    /// Fetches all Login ciphers for an organization and decrypts them to full [crate::CipherView].
160    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}