bitwarden_vault/cipher/cipher_client/admin/
get.rs1use 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
27pub 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 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 assert_eq!(result.ciphers.len(), 2);
213 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}