bitwarden_vault/cipher/cipher_client/admin/
get.rs1use 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 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 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}