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::{CryptoError, KeyStore};
4use bitwarden_error::bitwarden_error;
5use thiserror::Error;
6#[cfg(feature = "wasm")]
7use wasm_bindgen::prelude::wasm_bindgen;
8
9use crate::{
10    VaultParseError,
11    cipher::cipher::{ListOrganizationCiphersResult, PartialCipher},
12    cipher_client::admin::CipherAdminClient,
13};
14
15#[allow(missing_docs)]
16#[bitwarden_error(flat)]
17#[derive(Debug, Error)]
18pub enum GetOrganizationCiphersAdminError {
19    #[error(transparent)]
20    Crypto(#[from] CryptoError),
21    #[error(transparent)]
22    VaultParse(#[from] VaultParseError),
23    #[error(transparent)]
24    Api(#[from] ApiError),
25}
26
27/// Get all ciphers for an organization.
28pub async fn list_org_ciphers(
29    org_id: OrganizationId,
30    include_member_items: bool,
31    api_client: &bitwarden_api_api::apis::ApiClient,
32    key_store: &KeyStore<KeySlotIds>,
33) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
34    let api = api_client.ciphers_api();
35    let response: CipherMiniDetailsResponseModelListResponseModel = api
36        .get_organization_ciphers(Some(org_id.into()), Some(include_member_items))
37        .await
38        .map_err(ApiError::from)?;
39    let ciphers = response
40        .data
41        .into_iter()
42        .flatten()
43        .map(|model| model.merge_with_cipher(None))
44        .collect::<Result<Vec<_>, _>>()?;
45
46    let (list_views, _failures) = key_store.decrypt_list_with_failures(&ciphers);
47    Ok(ListOrganizationCiphersResult {
48        ciphers,
49        list_views,
50    })
51}
52
53#[cfg_attr(feature = "wasm", wasm_bindgen)]
54impl CipherAdminClient {
55    pub async fn list_org_ciphers(
56        &self,
57        org_id: OrganizationId,
58        include_member_items: bool,
59    ) -> Result<ListOrganizationCiphersResult, GetOrganizationCiphersAdminError> {
60        list_org_ciphers(
61            org_id,
62            include_member_items,
63            &self.client.internal.get_api_configurations().api_client,
64            self.client.internal.get_key_store(),
65        )
66        .await
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use bitwarden_api_api::{
73        apis::ApiClient,
74        models::{CipherMiniDetailsResponseModel, CipherMiniDetailsResponseModelListResponseModel},
75    };
76    use bitwarden_core::key_management::{KeySlotIds, SymmetricKeySlotId};
77    use bitwarden_crypto::{KeyStore, SymmetricCryptoKey};
78    use chrono::Utc;
79
80    use super::*;
81    use crate::{Cipher, CipherType, Login};
82
83    const TEST_ORG_ID: &str = "1bc9ac1e-f5aa-45f2-94bf-b181009709b8";
84    const TEST_CIPHER_ID_1: &str = "5faa9684-c793-4a2d-8a12-b33900187097";
85    const TEST_CIPHER_ID_2: &str = "6faa9684-c793-4a2d-8a12-b33900187098";
86
87    fn generate_test_cipher() -> Cipher {
88        Cipher {
89            id: TEST_CIPHER_ID_1.parse().ok(),
90            name: "2.pMS6/icTQABtulw52pq2lg==|XXbxKxDTh+mWiN1HjH2N1w==|Q6PkuT+KX/axrgN9ubD5Ajk2YNwxQkgs3WJM0S0wtG8=".parse().unwrap(),
91            r#type: CipherType::Login,
92            notes: Default::default(),
93            organization_id: Default::default(),
94            folder_id: Default::default(),
95            favorite: Default::default(),
96            reprompt: Default::default(),
97            fields: Default::default(),
98            collection_ids: Default::default(),
99            key: Default::default(),
100            login: Some(Login {
101                username: None,
102                password: None,
103                password_revision_date: None,
104                uris: None,
105                totp: None,
106                autofill_on_page_load: None,
107                fido2_credentials: None,
108            }),
109            identity: Default::default(),
110            card: Default::default(),
111            secure_note: Default::default(),
112            ssh_key: Default::default(),
113            bank_account: Default::default(),
114            organization_use_totp: Default::default(),
115            edit: Default::default(),
116            permissions: Default::default(),
117            view_password: Default::default(),
118            local_data: Default::default(),
119            attachments: Default::default(),
120            password_history: Default::default(),
121            creation_date: Default::default(),
122            deleted_date: Default::default(),
123            revision_date: Default::default(),
124            archived_date: Default::default(),
125            data: Default::default(),
126        }
127    }
128
129    fn mock_api_response(cipher: &Cipher) -> CipherMiniDetailsResponseModel {
130        CipherMiniDetailsResponseModel {
131            id: cipher.id.map(|id| id.into()),
132            name: Some(cipher.name.to_string()),
133            r#type: Some(cipher.r#type.into()),
134            login: cipher.login.clone().map(|l| Box::new(l.into())),
135            creation_date: Some(Utc::now().to_rfc3339()),
136            revision_date: Some(Utc::now().to_rfc3339()),
137            ..Default::default()
138        }
139    }
140
141    fn setup_key_store() -> KeyStore<KeySlotIds> {
142        let store: KeyStore<KeySlotIds> = KeyStore::default();
143        #[allow(deprecated)]
144        let _ = store.context_mut().set_symmetric_key(
145            SymmetricKeySlotId::User,
146            SymmetricCryptoKey::make_aes256_cbc_hmac_key(),
147        );
148        store
149    }
150
151    #[tokio::test]
152    async fn test_list_org_ciphers_all_success() {
153        let cipher_1 = generate_test_cipher();
154        let mut cipher_2 = generate_test_cipher();
155        cipher_2.id = TEST_CIPHER_ID_2.parse().ok();
156
157        let response_1 = mock_api_response(&cipher_1);
158        let response_2 = mock_api_response(&cipher_2);
159
160        let api_client = ApiClient::new_mocked(move |mock| {
161            mock.ciphers_api
162                .expect_get_organization_ciphers()
163                .returning(move |_org_id, _include_member_items| {
164                    Ok(CipherMiniDetailsResponseModelListResponseModel {
165                        object: None,
166                        data: Some(vec![response_1.clone(), response_2.clone()]),
167                        continuation_token: None,
168                    })
169                });
170        });
171
172        let store = setup_key_store();
173        let result = list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true, &api_client, &store)
174            .await
175            .unwrap();
176
177        assert_eq!(result.ciphers.len(), 2);
178        assert_eq!(result.list_views.len(), 2);
179        assert_eq!(result.ciphers[0].id, TEST_CIPHER_ID_1.parse().ok());
180        assert_eq!(result.ciphers[1].id, TEST_CIPHER_ID_2.parse().ok());
181    }
182
183    #[tokio::test]
184    async fn test_list_org_ciphers_with_failures() {
185        let cipher = generate_test_cipher();
186        let mut cipher_with_bad_key = generate_test_cipher();
187        cipher_with_bad_key.id = TEST_CIPHER_ID_2.parse().ok();
188
189        let response_good = mock_api_response(&cipher);
190        let mut response_bad = mock_api_response(&cipher_with_bad_key);
191        // Set an invalid key to cause decryption failure
192        response_bad.key = Some("2.Gg8yCM4IIgykCZyq0O4+cA==|GJLBtfvSJTDJh/F7X4cJPkzI6ccnzJm5DYl3yxOW2iUn7DgkkmzoOe61sUhC5dgVdV0kFqsZPcQ0yehlN1DDsFIFtrb4x7LwzJNIkMgxNyg=|1rGkGJ8zcM5o5D0aIIwAyLsjMLrPsP3EWm3CctBO3Fw=".to_string());
193
194        let api_client = ApiClient::new_mocked(move |mock| {
195            mock.ciphers_api
196                .expect_get_organization_ciphers()
197                .returning(move |_org_id, _include_member_items| {
198                    Ok(CipherMiniDetailsResponseModelListResponseModel {
199                        object: None,
200                        data: Some(vec![response_good.clone(), response_bad.clone()]),
201                        continuation_token: None,
202                    })
203                });
204        });
205
206        let store = setup_key_store();
207        let result = list_org_ciphers(TEST_ORG_ID.parse().unwrap(), true, &api_client, &store)
208            .await
209            .unwrap();
210
211        // All ciphers should be returned (both good and bad)
212        assert_eq!(result.ciphers.len(), 2);
213        // Only the good cipher should decrypt successfully
214        assert_eq!(result.list_views.len(), 1);
215    }
216
217    #[tokio::test]
218    async fn test_list_org_ciphers_empty() {
219        let api_client = ApiClient::new_mocked(move |mock| {
220            mock.ciphers_api
221                .expect_get_organization_ciphers()
222                .returning(move |_org_id, _include_member_items| {
223                    Ok(CipherMiniDetailsResponseModelListResponseModel {
224                        object: None,
225                        data: Some(vec![]),
226                        continuation_token: None,
227                    })
228                });
229        });
230
231        let store = setup_key_store();
232        let result = list_org_ciphers(TEST_ORG_ID.parse().unwrap(), false, &api_client, &store)
233            .await
234            .unwrap();
235
236        assert!(result.ciphers.is_empty());
237        assert!(result.list_views.is_empty());
238    }
239}