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::{ListOrganizationCiphersResult, PartialCipher, StrictDecrypt},
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#[cfg_attr(feature = "wasm", wasm_bindgen)]
70impl CipherAdminClient {
71    /// Fetches and decrypts all ciphers assigned to the current user for an organization.
72    pub async fn list_assigned_org_ciphers(
73        &self,
74        org_id: OrganizationId,
75    ) -> Result<ListOrganizationCiphersResult, GetAssignedOrgCiphersAdminError> {
76        use bitwarden_api_api::models::CipherDetailsResponseModelListResponseModel;
77
78        let response: CipherDetailsResponseModelListResponseModel = self
79            .api_configurations
80            .api_client
81            .ciphers_api()
82            .get_assigned_organization_ciphers(Some(org_id.into()))
83            .await?;
84
85        let ciphers = response
86            .data
87            .into_iter()
88            .flatten()
89            .map(|model| model.merge_with_cipher(None))
90            .collect::<Result<Vec<_>, _>>()?;
91
92        let list_views = if self.is_strict_decrypt().await {
93            let wrapped: Vec<StrictDecrypt<Cipher>> =
94                ciphers.iter().cloned().map(StrictDecrypt).collect();
95            let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&wrapped);
96            list_views
97        } else {
98            let (list_views, _failures) = self.key_store.decrypt_list_with_failures(&ciphers);
99            list_views
100        };
101        Ok(ListOrganizationCiphersResult {
102            ciphers,
103            list_views,
104        })
105    }
106
107    /// Get all ciphers for an organization.
108    pub async fn list_org_ciphers(
109        &self,
110        org_id: OrganizationId,
111        include_member_items: bool,
112    ) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
113        list_org_ciphers(
114            org_id,
115            include_member_items,
116            &self.api_configurations.api_client,
117            &self.key_store,
118            self.is_strict_decrypt().await,
119        )
120        .await
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use std::sync::Arc;
127
128    use bitwarden_api_api::{
129        apis::ApiClient,
130        models::{
131            CipherDetailsResponseModel, CipherDetailsResponseModelListResponseModel,
132            CipherMiniDetailsResponseModel, CipherMiniDetailsResponseModelListResponseModel,
133        },
134    };
135    use bitwarden_core::{
136        client::ApiConfigurations, key_management::create_test_crypto_with_user_key,
137    };
138    use bitwarden_crypto::{SymmetricCryptoKey, SymmetricKeyAlgorithm};
139    use chrono::Utc;
140
141    use super::*;
142    use crate::{Cipher, CipherType, Login};
143
144    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
145    const TEST_CIPHER_ID_1: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
146    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
147
148    fn create_test_client(api_client: ApiClient) -> CipherAdminClient {
149        #[allow(deprecated)]
150        CipherAdminClient {
151            key_store: create_test_crypto_with_user_key(SymmetricCryptoKey::make(
152                SymmetricKeyAlgorithm::Aes256CbcHmac,
153            )),
154            api_configurations: Arc::new(ApiConfigurations::from_api_client(api_client)),
155            client: bitwarden_core::Client::new_test(None),
156        }
157    }
158
159    fn mock_mini_cipher(cipher_id: &str) -> CipherMiniDetailsResponseModel {
160        let cipher = generate_test_cipher();
161        CipherMiniDetailsResponseModel {
162            id: cipher_id.parse().ok(),
163            name: cipher.name.as_ref().map(ToString::to_string),
164            r#type: Some(cipher.r#type.into()),
165            login: cipher.login.clone().map(|l| Box::new(l.into())),
166            creation_date: Some(Utc::now().to_rfc3339()),
167            revision_date: Some(Utc::now().to_rfc3339()),
168            ..Default::default()
169        }
170    }
171
172    fn mock_details_cipher(cipher_id: &str) -> CipherDetailsResponseModel {
173        CipherDetailsResponseModel {
174            id: Some(cipher_id.parse().unwrap()),
175            name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".to_string()),
176            r#type: Some(bitwarden_api_api::models::CipherType::Login),
177            login: Some(Box::new(bitwarden_api_api::models::CipherLoginModel::default())),
178            creation_date: Some(Utc::now().to_rfc3339()),
179            revision_date: Some(Utc::now().to_rfc3339()),
180            ..Default::default()
181        }
182    }
183
184    fn generate_test_cipher() -> Cipher {
185        Cipher {
186            id: TEST_CIPHER_ID_1.parse().ok(),
187            name: Some("2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap()),
188            r#type: CipherType::Login,
189            notes: Default::default(),
190            organization_id: Default::default(),
191            folder_id: Default::default(),
192            favorite: Default::default(),
193            reprompt: Default::default(),
194            fields: Default::default(),
195            collection_ids: Default::default(),
196            key: Default::default(),
197            login: Some(Login {
198                username: None,
199                password: None,
200                password_revision_date: None,
201                uris: None,
202                totp: None,
203                autofill_on_page_load: None,
204                fido2_credentials: None,
205            }),
206            identity: Default::default(),
207            card: Default::default(),
208            secure_note: Default::default(),
209            ssh_key: Default::default(),
210            bank_account: Default::default(),
211            drivers_license: Default::default(),
212            passport: Default::default(),
213            organization_use_totp: Default::default(),
214            edit: Default::default(),
215            permissions: Default::default(),
216            view_password: Default::default(),
217            local_data: Default::default(),
218            attachments: Default::default(),
219            password_history: Default::default(),
220            creation_date: Default::default(),
221            deleted_date: Default::default(),
222            revision_date: Default::default(),
223            archived_date: Default::default(),
224            data: Default::default(),
225        }
226    }
227
228    #[tokio::test]
229    async fn test_list_org_ciphers_all_success() {
230        let api_client = ApiClient::new_mocked(move |mock| {
231            mock.ciphers_api
232                .expect_get_organization_ciphers()
233                .returning(move |_org_id, _include_member_items| {
234                    Ok(CipherMiniDetailsResponseModelListResponseModel {
235                        object: None,
236                        data: Some(vec![
237                            mock_mini_cipher(TEST_CIPHER_ID_1),
238                            mock_mini_cipher(TEST_CIPHER_ID_2),
239                        ]),
240                        continuation_token: None,
241                    })
242                });
243        });
244
245        let client = create_test_client(api_client);
246        let result = client
247            .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
248            .await
249            .unwrap();
250
251        assert_eq!(result.ciphers.len(), 2);
252        assert_eq!(result.list_views.len(), 2);
253        assert_eq!(result.ciphers[0].id, TEST_CIPHER_ID_1.parse().ok());
254        assert_eq!(result.ciphers[1].id, TEST_CIPHER_ID_2.parse().ok());
255    }
256
257    #[tokio::test]
258    async fn test_list_org_ciphers_with_failures() {
259        let api_client = ApiClient::new_mocked(move |mock| {
260            mock.ciphers_api
261                .expect_get_organization_ciphers()
262                .returning(move |_org_id, _include_member_items| {
263                    let mut bad = mock_mini_cipher(TEST_CIPHER_ID_2);
264                    bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
265                    Ok(CipherMiniDetailsResponseModelListResponseModel {
266                        object: None,
267                        data: Some(vec![mock_mini_cipher(TEST_CIPHER_ID_1), bad]),
268                        continuation_token: None,
269                    })
270                });
271        });
272
273        let client = create_test_client(api_client);
274        let result = client
275            .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true)
276            .await
277            .unwrap();
278
279        assert_eq!(result.ciphers.len(), 2);
280        assert_eq!(result.list_views.len(), 1);
281    }
282
283    #[tokio::test]
284    async fn test_list_org_ciphers_empty() {
285        let api_client = ApiClient::new_mocked(move |mock| {
286            mock.ciphers_api
287                .expect_get_organization_ciphers()
288                .returning(move |_org_id, _include_member_items| {
289                    Ok(CipherMiniDetailsResponseModelListResponseModel {
290                        object: None,
291                        data: Some(vec![]),
292                        continuation_token: None,
293                    })
294                });
295        });
296
297        let client = create_test_client(api_client);
298        let result = client
299            .list_org_ciphers(TEST_ORG_ID.parse().unwrap(), false)
300            .await
301            .unwrap();
302
303        assert!(result.ciphers.is_empty());
304        assert!(result.list_views.is_empty());
305    }
306
307    #[tokio::test]
308    async fn test_list_assigned_org_ciphers_success() {
309        let api_client = ApiClient::new_mocked(|mock| {
310            mock.ciphers_api
311                .expect_get_assigned_organization_ciphers()
312                .returning(|_| {
313                    Ok(CipherDetailsResponseModelListResponseModel {
314                        object: None,
315                        data: Some(vec![
316                            mock_details_cipher(TEST_CIPHER_ID_1),
317                            mock_details_cipher(TEST_CIPHER_ID_2),
318                        ]),
319                        continuation_token: None,
320                    })
321                });
322        });
323
324        let client = create_test_client(api_client);
325        let result = client
326            .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
327            .await
328            .unwrap();
329
330        assert_eq!(result.ciphers.len(), 2);
331        assert_eq!(result.list_views.len(), 2);
332    }
333
334    #[tokio::test]
335    async fn test_list_assigned_org_ciphers_with_failures() {
336        let api_client = ApiClient::new_mocked(|mock| {
337            mock.ciphers_api
338                .expect_get_assigned_organization_ciphers()
339                .returning(|_| {
340                    let mut bad = mock_details_cipher(TEST_CIPHER_ID_2);
341                    bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
342                    Ok(CipherDetailsResponseModelListResponseModel {
343                        object: None,
344                        data: Some(vec![mock_details_cipher(TEST_CIPHER_ID_1), bad]),
345                        continuation_token: None,
346                    })
347                });
348        });
349
350        let client = create_test_client(api_client);
351        let result = client
352            .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
353            .await
354            .unwrap();
355
356        assert_eq!(result.ciphers.len(), 2);
357        assert_eq!(result.list_views.len(), 1);
358        assert_eq!(result.list_views[0].id, TEST_CIPHER_ID_1.parse().ok());
359    }
360
361    #[tokio::test]
362    async fn test_list_assigned_org_ciphers_empty() {
363        let api_client = ApiClient::new_mocked(|mock| {
364            mock.ciphers_api
365                .expect_get_assigned_organization_ciphers()
366                .returning(|_| {
367                    Ok(CipherDetailsResponseModelListResponseModel {
368                        object: None,
369                        data: Some(vec![]),
370                        continuation_token: None,
371                    })
372                });
373        });
374
375        let client = create_test_client(api_client);
376        let result = client
377            .list_assigned_org_ciphers(TEST_ORG_ID.parse().unwrap())
378            .await
379            .unwrap();
380
381        assert!(result.ciphers.is_empty());
382        assert!(result.list_views.is_empty());
383    }
384}