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
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
35/// Get all ciphers for an organization.
36pub 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
69/// Get all Login ciphers for an organization, decrypted to full [crate::CipherView].
70pub 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    /// Fetches and decrypts all ciphers assigned to the current user for an organization.
106    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    /// Get all ciphers for an organization.
142    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    /// Fetches all Login ciphers for an organization and decrypts them to full [crate::CipherView].
158    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}