Skip to main content

bitwarden_vault/cipher/cipher_client/admin/
get.rs

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