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